hyper-optimized-telemetry

This commit is contained in:
Ben Harris 2021-11-10 15:19:52 -05:00
parent 588f26458c
commit 6184560fbf
9 changed files with 507 additions and 0 deletions

View File

@ -89,6 +89,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwoFer", "csharp\two-fer\Tw
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WizardsAndWarriors", "csharp\wizards-and-warriors\WizardsAndWarriors.csproj", "{240CFD47-2AEC-49F2-B0B2-884B45E9A27A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HyperOptimizedTelemetry", "csharp\hyper-optimized-telemetry\HyperOptimizedTelemetry.csproj", "{2AF7AFB0-D34C-4B5E-B1F2-A07475BE5B52}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -267,6 +269,10 @@ Global
{240CFD47-2AEC-49F2-B0B2-884B45E9A27A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{240CFD47-2AEC-49F2-B0B2-884B45E9A27A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{240CFD47-2AEC-49F2-B0B2-884B45E9A27A}.Release|Any CPU.Build.0 = Release|Any CPU
{2AF7AFB0-D34C-4B5E-B1F2-A07475BE5B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2AF7AFB0-D34C-4B5E-B1F2-A07475BE5B52}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2AF7AFB0-D34C-4B5E-B1F2-A07475BE5B52}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2AF7AFB0-D34C-4B5E-B1F2-A07475BE5B52}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -0,0 +1,21 @@
{
"blurb": "Learn about bit conversion by implementing a message protocol.",
"contributors": [
"ErikSchierboom",
"yzAlvin"
],
"authors": [
"mikedamay"
],
"files": {
"solution": [
"HyperOptimizedTelemetry.cs"
],
"test": [
"HyperOptimizedTelemetryTests.cs"
],
"exemplar": [
".meta/Exemplar.cs"
]
}
}

View File

@ -0,0 +1 @@
{"track":"csharp","exercise":"hyper-optimized-telemetry","id":"c01516a8855647618a6e1d8bb9d1884f","url":"https://exercism.org/tracks/csharp/exercises/hyper-optimized-telemetry","handle":"benharri","is_requester":true,"auto_approve":false}

View File

@ -0,0 +1,39 @@
# Help
## Running the tests
You can run the tests by opening a command prompt in the exercise's directory, and then running the [`dotnet test` command](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-test)
Alternatively, most IDE's have built-in support for running tests, including [Visual Studio](https://docs.microsoft.com/en-us/visualstudio/test/run-unit-tests-with-test-explorer), [Rider](https://www.jetbrains.com/help/rider/Unit_Testing_in_Solution.html) and [Visual Studio code](https://github.com/OmniSharp/omnisharp-vscode/wiki/How-to-run-and-debug-unit-tests).
See the [tests page](https://exercism.io/tracks/csharp/tests) for more information.
## Skipped tests
Initially, only the first test will be enabled.
This is to encourage you to solve the exercise one step at a time.
Once you get the first test passing, remove the `Skip` property from the next test and work on getting that test passing.
## Submitting your solution
You can submit your solution using the `exercism submit HyperOptimizedTelemetry.cs` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [C# track's documentation](https://exercism.org/docs/tracks/csharp)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
To get help if you're having trouble, you can use one of the following resources:
- [Gitter](https://gitter.im/exercism/xcsharp) is Exercism C# track's Gitter room; go here to get support and ask questions related to the C# track.
- [/r/csharp](https://www.reddit.com/r/csharp) is the C# subreddit.
- [StackOverflow](http://stackoverflow.com/questions/tagged/c%23) can be used to search for your problem and see if it has been answered already. You can also ask and answer questions.

View File

@ -0,0 +1,19 @@
# Hints
## General
- [Integral numeric types][integral-numeric-types]: overview of the integral numeric types.
- [Numeric conversions][numeric-conversions]: overview of implicit and explicit numeric conversions.
## 1. Encode an integral value ready to send
- Conversion to a byte array is dealt with [here][bit-converter-get-bytes]
## 2. Decode a received buffer
- Converting from a byte array is discussed [here][bit-converter-to-type]
[integral-numeric-types]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/integral-numeric-types
[numeric-conversions]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/numeric-conversions
[bit-converter-get-bytes]: https://docs.microsoft.com/en-us/dotnet/api/system.bitconverter.getbytes?view=netcore-3.1
[bit-converter-to-type]: https://docs.microsoft.com/en-us/dotnet/api/system.bitconverter.toint16?view=netcore-3.1

View File

@ -0,0 +1,34 @@
using System;
public static class TelemetryBuffer
{
public static byte[] ToBuffer(long reading)
{
var result = new byte[9];
var bytesOfReading = BitConverter.GetBytes(reading);
sbyte size = reading switch
{
> uint.MaxValue or < int.MinValue => -8,
> int.MaxValue => 4,
> ushort.MaxValue or < short.MinValue => -4,
>= 0 => 2,
>= short.MinValue => -2
};
Array.Resize(ref bytesOfReading, Math.Abs(size));
result[0] = (byte)size;
bytesOfReading.CopyTo(result, 1);
return result;
}
public static long FromBuffer(byte[] buffer) =>
(sbyte)buffer[0] switch
{
-8 => BitConverter.ToInt64(buffer, 1),
4 => BitConverter.ToUInt32(buffer, 1),
-4 => BitConverter.ToInt32(buffer, 1),
2 => BitConverter.ToUInt16(buffer, 1),
-2 => BitConverter.ToInt16(buffer, 1),
_ => 0
};
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Exercism.Tests" Version="0.1.0-beta1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,271 @@
using System;
using Xunit;
using Exercism.Tests;
public class HyperOptimizedTelemetryTests
{
[Fact]
[Task(1)]
public void ToBuffer_upper_long()
{
var bytes = TelemetryBuffer.ToBuffer(Int64.MaxValue);
Assert.Equal(new byte[] { 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_lower_long()
{
var bytes = TelemetryBuffer.ToBuffer((long)UInt32.MaxValue + 1);
Assert.Equal(new byte[] { 0xf8, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_upper_uint()
{
var bytes = TelemetryBuffer.ToBuffer(UInt32.MaxValue);
Assert.Equal(new byte[] { 0x4, 0xff, 0xff, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_lower_uint()
{
var bytes = TelemetryBuffer.ToBuffer((long)Int32.MaxValue + 1);
Assert.Equal(new byte[] { 0x4, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_upper_int()
{
var bytes = TelemetryBuffer.ToBuffer(Int32.MaxValue);
Assert.Equal(new byte[] { 0xfc, 0xff, 0xff, 0xff, 0x7f, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_lower_int()
{
var bytes = TelemetryBuffer.ToBuffer((long)UInt16.MaxValue + 1);
Assert.Equal(new byte[] { 0xfc, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_upper_ushort()
{
var bytes = TelemetryBuffer.ToBuffer(UInt16.MaxValue);
Assert.Equal(new byte[] { 0x2, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_lower_ushort()
{
var bytes = TelemetryBuffer.ToBuffer((long)Int16.MaxValue + 1);
Assert.Equal(new byte[] { 0x2, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_upper_short()
{
var bytes = TelemetryBuffer.ToBuffer(Int16.MaxValue);
Assert.Equal(new byte[] { 0x2, 0xff, 0x7f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_Zero()
{
var bytes = TelemetryBuffer.ToBuffer(0);
Assert.Equal(new byte[] { 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_upper_neg_short()
{
var bytes = TelemetryBuffer.ToBuffer(-1);
Assert.Equal(new byte[] { 0xfe, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_lower_neg_short()
{
var bytes = TelemetryBuffer.ToBuffer(Int16.MinValue);
Assert.Equal(new byte[] { 0xfe, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_upper_neg_int()
{
int n = Int16.MinValue - 1;
var bytes = TelemetryBuffer.ToBuffer(n);
Assert.Equal(new byte[] { 0xfc, 0xff, 0x7f, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_lower_neg_int()
{
var bytes = TelemetryBuffer.ToBuffer(Int32.MinValue);
Assert.Equal(new byte[] { 0xfc, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0 }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_upper_neg_long()
{
var bytes = TelemetryBuffer.ToBuffer((long)Int32.MinValue - 1);
Assert.Equal(new byte[] { 0xf8, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff }, bytes);
}
[Fact]
[Task(1)]
public void ToBuffer_lower_neg_long()
{
var bytes = TelemetryBuffer.ToBuffer(Int64.MinValue);
Assert.Equal(new byte[] { 0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80 }, bytes);
}
[Fact]
[Task(2)]
public void FromBuffer_Invalid()
{
Assert.Equal(0,
TelemetryBuffer.FromBuffer(new byte[] { 22, 0xff, 0xff, 0xff, 0x7f, 0, 0, 0, 0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_upper_long()
{
Assert.Equal(Int64.MaxValue,
TelemetryBuffer.FromBuffer(new byte[] { 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f }));
}
[Fact]
[Task(2)]
public void FromBuffer_lower_long()
{
Assert.Equal((long)UInt32.MaxValue + 1,
TelemetryBuffer.FromBuffer(new byte[] { 0xf8, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_upper_uint()
{
Assert.Equal(UInt32.MaxValue,
TelemetryBuffer.FromBuffer(new byte[] { 0x4, 0xff, 0xff, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_lower_uint()
{
Assert.Equal((long)Int32.MaxValue + 1,
TelemetryBuffer.FromBuffer(new byte[] { 0x4, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_upper_int()
{
Assert.Equal(Int32.MaxValue,
TelemetryBuffer.FromBuffer(new byte[] { 0xfc, 0xff, 0xff, 0xff, 0x7f, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_lower_int()
{
Assert.Equal(UInt16.MaxValue + 1,
TelemetryBuffer.FromBuffer(new byte[] { 0xfc, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_upper_ushort()
{
Assert.Equal(UInt16.MaxValue,
TelemetryBuffer.FromBuffer(new byte[] { 0x2, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_lower_ushort()
{
Assert.Equal(Int16.MaxValue + 1,
TelemetryBuffer.FromBuffer(new byte[] { 0x2, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_upper_short()
{
Assert.Equal(Int16.MaxValue,
TelemetryBuffer.FromBuffer(new byte[] { 0xfe, 0xff, 0x7f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_Zero()
{
Assert.Equal(0,
TelemetryBuffer.FromBuffer(new byte[] { 0xfe, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_upper_neg_short()
{
Assert.Equal(-1,
TelemetryBuffer.FromBuffer(new byte[] { 0xfe, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_lower_neg_short()
{
Assert.Equal(Int16.MinValue,
TelemetryBuffer.FromBuffer(new byte[] { 0xfe, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_upper_neg_int()
{
Assert.Equal(Int16.MinValue - 1,
TelemetryBuffer.FromBuffer(new byte[] { 0xfc, 0xff, 0x7f, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_lower_neg_int()
{
Assert.Equal(Int32.MinValue,
TelemetryBuffer.FromBuffer(new byte[] { 0xfc, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0 }));
}
[Fact]
[Task(2)]
public void FromBuffer_upper_neg_long()
{
Assert.Equal((long)Int32.MinValue - 1,
TelemetryBuffer.FromBuffer(new byte[] { 0xf8, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff }));
}
[Fact]
[Task(2)]
public void FromBuffer_lower_neg_long()
{
Assert.Equal(Int64.MinValue,
TelemetryBuffer.FromBuffer(new byte[] { 0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80 }));
}
}

View File

@ -0,0 +1,102 @@
# Hyper-optimized Telemetry
Welcome to Hyper-optimized Telemetry on Exercism's C# Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
C#, like many statically typed languages, provides a number of types that represent integers, each with its own range of values. At the low end, the `sbyte` type has a minimum value of -128 and a maximum value of 127. Like all the integer types these values are available as `<type>.MinValue` and `<type>.MaxValue`. At the high end, the `long` type has a minimum value of -9,223,372,036,854,775,808 and a maximum value of 9,223,372,036,854,775,807. In between lie the `short` and `int` types.
The ranges are determined by the storage width of the type as allocated by the system. For example, a `byte` uses 8 bits and a `long` uses 64 bits.
Each of the above types is paired with an unsigned equivalent: `sbyte`/`byte`, `short`/`ushort`, `int`/`uint` and `long`/`ulong`. In all cases the range of the values is from 0 to the negative signed maximum times 2 plus 1.
| Type | Width | Minimum | Maximum |
| ------ | ------ | -------------------------- | --------------------------- |
| sbyte | 8 bit | -128 | +127 |
| short | 16 bit | -32_768 | +32_767 |
| int | 32 bit | -2_147_483_648 | +2_147_483_647 |
| long | 64 bit | -9_223_372_036_854_775_808 | +9_223_372_036_854_775_807 |
| byte | 8 bit | 0 | +255 |
| ushort | 16 bit | 0 | +65_535 |
| uint | 32 bit | 0 | +4_294_967_295 |
| ulong | 64 bit | 0 | +18_446_744_073_709_551_615 |
A variable (or expression) of one type can easily be converted to another. For instance, in an assignment operation, if the type of the value being assigned (lhs) ensures that the value will lie within the range of the type being assigned to (rhs) then there is a simple assignment:
```csharp
uint ui = uint.MaxValue;
ulong ul = ui; // no problem
```
On the other hand if the range of type being assigned from is not a subset of the assignee's range of values then a cast, `()` operation is required even if the particular value is within the assignee's range:
```csharp
short s = 42;
uint ui = (uint)s;
```
## Bit conversion
The `BitConverter` class provides a convenient way of converting integer types to and from arrays of bytes.
## Instructions
Work continues on the remote control car project. Bandwidth in the telemetry system is at a premium and you have been asked to implement a message protocol for communicating telemetry data.
Data is transmitted in a buffer (byte array). When integers are sent, the size of the buffer is reduced by employing the protocol described below.
Each value should be represented in the smallest possible integral type (types of `byte` and `sbyte` are not included as the saving would be trivial):
| From | To | Type |
| -------------------------- | ------------------------- | -------- |
| 4_294_967_296 | 9_223_372_036_854_775_807 | `long` |
| 2_147_483_648 | 4_294_967_295 | `uint` |
| 65_536 | 2_147_483_647 | `int` |
| 0 | 65_535 | `ushort` |
| -32_768 | -1 | `short` |
| -2_147_483_648 | -32_769 | `int` |
| -9_223_372_036_854_775_808 | -2_147_483_649 | `long` |
The value should be converted to the appropriate number of bytes for its assigned type. The complete buffer comprises a byte indicating the number of additional bytes in the buffer (_prefix byte_) followed by the bytes holding the integer (_payload bytes_).
Some of the types use an identical number of bytes (e.g. the `uint` and `int` types). Normally, they would have the same _prefix byte_, but that would make decoding problematic. To counter this, the protocol introduces a little trick: for signed types, their _prefix byte_ value is `256` minus the number of additional bytes in the buffer.
Only the prefix byte and the number of following bytes indicated by the prefix will be sent in the communication. Internally a 9 byte buffer is used (with trailing zeroes, as necessary) both by sending and receiving routines.
## 1. Encode an integral value ready to send
Please implement the static method `TelemetryBuffer.ToBuffer()` to encode a buffer taking the parameter passed to the method.
```csharp
// Type: ushort, bytes: 2, signed: no, prefix byte: 2
TelemetryBuffer.ToBuffer(5)
// => {0x2, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 };
// Type: int, bytes: 4, signed: yes, prefix byte: 256 - 4
TelemetryBuffer.ToBuffer(Int32.MaxValue)
// => {0xfc, 0xff, 0xff, 0xff, 0x7f, 0x0, 0x0, 0x0, 0x0 };
```
## 2. Decode a received buffer
Please implement the static method `TelemetryBuffer.FromBuffer()` to decode the buffer received and return the value in the form of a `long`.
```csharp
TelemetryBuffer.FromBuffer(new byte[] {0xfc, 0xff, 0xff, 0xff, 0x7f, 0x0, 0x0, 0x0, 0x0 })
// => 2147483647
```
If the prefix byte is not one of `-8`, `-4`, `-2`, `2` or `4` then `0` should be returned.
## Source
### Created by
- @mikedamay
### Contributed to by
- @ErikSchierboom
- @yzAlvin