Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite System.Text.Json stream tests to be async friendly and enable on WASM #38663

Merged
merged 8 commits into from
Jul 10, 2020

Conversation

akoeplinger
Copy link
Member

@akoeplinger akoeplinger commented Jul 1, 2020

The tests are using a (De)SerializationWrapper so the same code can be used both for String and Stream types.
It does that by wrapping the async Stream serialization calls in Task.Run().GetAwaiter().GetResult() to turn them into sync calls.
However that doesn't work on WebAssembly since we can't wait on tasks as there's only a single thread.

To fix this inverse the wrapper so the synchronous String calls are turned into async and use normal awaits for the Stream calls.

This allows the test suite to pass on WebAssembly: Tests run: 8349, Errors: 0, Failures: 0, Skipped: 11. Time: 475.528706s

@stephentoub
Copy link
Member

We can skip fewer now, right? :)

@akoeplinger
Copy link
Member Author

Yeah I'm working on it :)

… on WASM

The tests dealing are using a (De)SerializationWrapper so the same code can be used both for String and Stream types.
It does that by wrapping the async Stream serialization calls in `Task.Run().GetAwaiter().GetResult()` to turn them into sync calls.
However that doesn't work on WebAssembly since we can't wait on tasks as there's only a single thread.

To fix this inverse the wrapper so the synchronous String calls are turned into async and use normal awaits for the Stream calls.

This allows the test suite to pass on WebAssembly: `Tests run: 8349, Errors: 0, Failures: 0, Skipped: 11. Time: 475.528706s`
@akoeplinger akoeplinger force-pushed the wasm-system-text-json branch from a560a84 to 99192c0 Compare July 3, 2020 21:31
@akoeplinger akoeplinger changed the title WASM: enable System.Text.Json tests Rewrite System.Text.Json stream tests to be async friendly and enable on WASM Jul 3, 2020
@akoeplinger akoeplinger marked this pull request as ready for review July 3, 2020 21:33
@akoeplinger
Copy link
Member Author

@stephentoub this is ready for review now :)

@ericstj
Copy link
Member

ericstj commented Jul 8, 2020

@steveharter @layomia PTAL

@@ -3650,7 +3647,7 @@ private static string GetCompactJson(TestCaseType testCaseType, string jsonStrin
return s_compactJson[testCaseType] = existing;
}

[Fact]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wouldn't be testing the exact same thing, but it seems like this test would "just work" if you changed the Task.WaitAll(tasks) to instead be await Task.WhenAll(tasks) and made the method async, no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that does indeed work. I can send a followup PR but I'd like to first get this one in while CI is green :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reenabled these tests with your suggestion in the latest iteration.

tasks[12] = Task.Run(async () => await RunTest<Point_With_Dictionary>(Point_With_Dictionary.s_data));
tasks[13] = Task.Run(async () => await RunTest<Point_With_Object>(Point_With_Object.s_data));

Task.WaitAll(tasks);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also apply the pattern suggested in https://github.com/dotnet/runtime/pull/38663/files#r452481795 here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the intent of this test to detect race conditions? By awaiting one-by-one doesn't defy said purpose?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't look to me like this test is about race conditions, but I changed it to the Task.WhenAll() pattern nonetheless.

@@ -1609,7 +1610,7 @@ public static void TestPartialJsonReaderSlicesSpecialNumbers(TestCaseType type,
}
}

[Theory]
[ConditionalTheory]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of a ConditionalTheory without parameters?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary so throwing SkipTestException works, otherwise the test would fail with an "unexpected" exception.

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than my comments, LGTM.

Copy link
Member Author

@akoeplinger akoeplinger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jozkee @stephentoub I addressed your feedback, please take another look :)

@@ -3650,7 +3647,7 @@ private static string GetCompactJson(TestCaseType testCaseType, string jsonStrin
return s_compactJson[testCaseType] = existing;
}

[Fact]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reenabled these tests with your suggestion in the latest iteration.

tasks[12] = Task.Run(async () => await RunTest<Point_With_Dictionary>(Point_With_Dictionary.s_data));
tasks[13] = Task.Run(async () => await RunTest<Point_With_Object>(Point_With_Object.s_data));

Task.WaitAll(tasks);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't look to me like this test is about race conditions, but I changed it to the Task.WhenAll() pattern nonetheless.

@@ -1609,7 +1610,7 @@ public static void TestPartialJsonReaderSlicesSpecialNumbers(TestCaseType type,
}
}

[Theory]
[ConditionalTheory]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary so throwing SkipTestException works, otherwise the test would fail with an "unexpected" exception.

…json

# Conflicts:
#	src/libraries/tests.proj
tasks[0] = Task.Run(async () => await RunTestAsync<Class_With_Ctor_With_65_Params>());
tasks[1] = Task.Run(async () => await RunTestAsync<Struct_With_Ctor_With_65_Params>());

await Task.WhenAll(tasks);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You made these run in parallel just for fun? :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were actually already running in parallel before this PR.

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than not being clear on why you added concurrency into some of the tests, LGTM.

@@ -121,7 +121,7 @@ void Test(int i)
tasks[i + 1] = Task.Run(() => Test(TestClassCount - 2));
}

Task.WaitAll(tasks);
await Task.WhenAll(tasks);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI this area of code really does care about threading, and the changes shouldn't affect the multi-threading aspects.

{
var obj = Serializer.Deserialize(json, type, options);
var obj = await Serializer.DeserializeAsync(json, type, options);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this changed to use the Async version of the API? The original intent was to test non-async APIs for thread safety.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original test was calling a synchronous method which was then overridden by two different implementations, one that used the underlying sync API and one that used the underlying async API and then blocked waiting on it. This change inverts that, so it's an async API that then uses the underlying sync API and wraps it in a task or the underlying async API. It's done to avoid sync-over-async, which blows up on wasm.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. I've tried to capture this in the PR description but please let me know if I could make this clearer.

{
if (options == null)
{
options = _optionsWithSmallBuffer;
}

return Task.Run(async () =>
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is the key area of code where we switch the wrapper to use await...

await JsonSerializer.SerializeAsync(stream, value, inputType, options);
return Encoding.UTF8.GetString(stream.ToArray());
}).GetAwaiter().GetResult();
using var stream = new MemoryStream();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this is the key area of code where we switch the wrapper to use await without Task.Run.

@steveharter
Copy link
Member

One global comment is that the renaming of Serialize to SerializeAsync and Deserialize to DeserializeAsync is now a bit confusing since it reads as being async when in the majority of cases it isn't. I realize we can't remove the 'await' modifier but perhaps these forms are more understandable:
await Serializer.Serialize(
await Serializer.SerializeSyncOrAsync(
await Serializer.SerializeWrapper(

@akoeplinger
Copy link
Member Author

@steveharter I switched to using Serializer.SerializeWrapper() instead of the Async suffix

@akoeplinger akoeplinger merged commit 95e3dcc into dotnet:master Jul 10, 2020
@akoeplinger akoeplinger deleted the wasm-system-text-json branch July 10, 2020 20:43
@ghost ghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants