Skip to content

Commit

Permalink
Implement serialization of ReadableStream
Browse files Browse the repository at this point in the history
Enable ReadableStream objects to be transferred by postMessage().
Internally, the ReadableStream is converted to a MessagePortChannel and
it is that that is transferred. This involves adding a "stream_channels"
field to the TransferableMessage mojo struct for the channels that will
be used by ReadableStream (and in future WritableStream and
TransferableStream objects).

Additional code is added to V8ScriptValueSerializer and
V8ScriptValueDeserializer to recognise ReadableStream objects.
SerializedScriptValue has new methods and an array of
MessagePortChannels as a member variable to transfer them.

The new code is behind the "TransferableStreams" Blink feature and must
be explicitly activated. A set of new layout tests provides basic
verification that postMessage() of ReadableStream objects work. They are
run with the feature enabled via a new VirtualTestSuite.

The changes are covered in more detail in the design doc:
https://docs.google.com/document/d/1_KuZzg5c3pncLJPFa8SuVm23AP4tft6mzPCL5at3I9M/edit

Bug: 894838
Cq-Include-Trybots: luci.chromium.try:linux_mojo
Change-Id: I032306415aa0c4e146af25439f42bcff70436416
Reviewed-on: https://chromium-review.googlesource.com/c/1315367
Commit-Queue: Adam Rice <ricea@chromium.org>
Reviewed-by: Yuki Shiino <yukishiino@chromium.org>
Reviewed-by: Jeremy Roman <jbroman@chromium.org>
Reviewed-by: Kinuko Yasuda <kinuko@chromium.org>
Reviewed-by: Yutaka Hirano <yhirano@chromium.org>
Cr-Commit-Position: refs/heads/master@{#609688}
  • Loading branch information
ricea authored and Commit Bot committed Nov 20, 2018
1 parent 96fcc57 commit a1e223e
Show file tree
Hide file tree
Showing 43 changed files with 592 additions and 17 deletions.
10 changes: 4 additions & 6 deletions content/browser/message_port_provider.cc
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,10 @@ void MessagePortProvider::PostMessageToFrame(
const base::android::JavaParamRef<jstring>& target_origin,
const base::android::JavaParamRef<jstring>& data,
const base::android::JavaParamRef<jobjectArray>& ports) {
PostMessageToFrameInternal(
web_contents,
ToString16(env, source_origin),
ToString16(env, target_origin),
ToString16(env, data),
AppWebMessagePort::UnwrapJavaArray(env, ports));
PostMessageToFrameInternal(web_contents, ToString16(env, source_origin),
ToString16(env, target_origin),
ToString16(env, data),
AppWebMessagePort::UnwrapJavaArray(env, ports));
}
#endif

Expand Down
2 changes: 2 additions & 0 deletions content/common/content_param_traits.cc
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ void ParamTraits<scoped_refptr<base::RefCountedData<
WriteParam(m, p->data.stack_trace_debugger_id_first);
WriteParam(m, p->data.stack_trace_debugger_id_second);
WriteParam(m, p->data.ports);
WriteParam(m, p->data.stream_channels);
WriteParam(m, p->data.has_user_gesture);
WriteParam(m, !!p->data.user_activation);
if (p->data.user_activation) {
Expand Down Expand Up @@ -241,6 +242,7 @@ bool ParamTraits<
!ReadParam(m, iter, &(*r)->data.stack_trace_debugger_id_first) ||
!ReadParam(m, iter, &(*r)->data.stack_trace_debugger_id_second) ||
!ReadParam(m, iter, &(*r)->data.ports) ||
!ReadParam(m, iter, &(*r)->data.stream_channels) ||
!ReadParam(m, iter, &(*r)->data.has_user_gesture) ||
!ReadParam(m, iter, &has_activation)) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ public void postMessage(String message, MessagePort[] sentPorts) throws IllegalS
msg.arrayBufferContentsArray = new SerializedArrayBufferContents[0];
msg.imageBitmapContentsArray = new Bitmap[0];
msg.ports = ports;
msg.streamChannels = new MessagePipeHandle[0];
mConnector.accept(msg.serializeWithHeader(mMojoCore, MESSAGE_HEADER));
}

Expand Down
5 changes: 5 additions & 0 deletions third_party/WebKit/LayoutTests/VirtualTestSuites
Original file line number Diff line number Diff line change
Expand Up @@ -1026,5 +1026,10 @@
"prefix": "not-site-per-process",
"base": "http/tests/xmlhttprequest/origin-whitelisting-ip-addresses.html",
"args": ["--disable-site-isolation-trials"]
},
{
"prefix": "transferable-streams",
"base": "http/tests/streams/transferable",
"args": ["--enable-blink-features=TransferableStreams"]
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script>
addEventListener('message', evt => {
evt.source.postMessage(evt.data, '*', [evt.data]);
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Create a ReadableStream that will pass the tests in
// testTransferredReadableStream(), below.
function createOriginalReadableStream() {
return new ReadableStream({
start(controller) {
controller.enqueue('a');
controller.close();
}
});
}

// Common tests to roughly determine that |rs| is a correctly transferred
// version of a stream created by createOriginalReadableStream().
function testTransferredReadableStream(rs) {
assert_equals(rs.constructor, ReadableStream,
'rs should be a ReadableStream in this realm');
assert_true(rs instanceof ReadableStream,
'instanceof check should pass');

// Perform a brand-check on |rs| in the process of calling getReader().
const reader = ReadableStream.prototype.getReader.call(rs);

return reader.read().then(({value, done}) => {
assert_false(done, 'done should be false');
assert_equals(value, 'a', 'value should be "a"');
return reader.read();
}).then(({value, done}) => {
assert_true(done, 'done should be true');
});
}

function testMessage(msg) {
assert_array_equals(msg.ports, [], 'there should be no ports in the event');
return testTransferredReadableStream(msg.data);
}

function testMessageEvent(target) {
return new Promise((resolve, reject) => {
target.addEventListener('message', ev => {
try {
resolve(testMessage(ev));
} catch(e) {
reject(e);
}
}, {once: true});
});
}

function testMessageEventOrErrorMessage(target) {
return new Promise((resolve, reject) => {
target.addEventListener('message', ev => {
if (typeof ev.data === 'string') {
// Assume it's an error message and reject with it.
reject(ev.data);
return;
}

try {
resolve(testMessage(ev));
} catch(e) {
reject(e);
}
}, {once: true});
});
}

function checkTestResults(target) {
return new Promise((resolve, reject) => {
target.onmessage = msg => {
// testharness.js sends us objects which we need to ignore.
if (typeof msg.data !== 'string')
return;

if (msg.data === 'OK') {
resolve();
} else {
reject(msg.data);
}
};
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';
importScripts('/resources/testharness.js', 'helpers.js');

onconnect = evt => {
const port = evt.source;
const promise = testMessageEvent(port);
port.start();
promise
.then(() => port.postMessage('OK'))
.catch(err => port.postMessage(`BAD: ${err}`));
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';
importScripts('/resources/testharness.js', 'helpers.js');

const promise = testMessageEvent(self);
promise
.then(() => postMessage('OK'))
.catch(err => postMessage(`BAD: ${err}`));
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';
importScripts('helpers.js');

onconnect = msg => {
const port = msg.source;
const orig = createOriginalReadableStream();
try {
port.postMessage(orig, [orig]);
} catch (e) {
port.postMessage(e.message);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';
importScripts('helpers.js');

const orig = createOriginalReadableStream();
postMessage(orig, [orig]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script src="/resources/testharness.js"></script>
<script src="helpers.js"></script>
<script>
'use strict';

setup({
explicit_done: true
});

function startTests() {
promise_test(() => {
const orig = createOriginalReadableStream();
const promise = checkTestResults(navigator.serviceWorker);
navigator.serviceWorker.controller.postMessage(orig, [orig]);
assert_true(orig.locked, 'the original stream should be locked');
return promise;
}, 'serviceWorker.controller.postMessage should be able to transfer a ' +
'ReadableStream');

promise_test(() => {
const promise = testMessageEventOrErrorMessage(navigator.serviceWorker);
navigator.serviceWorker.controller.postMessage('SEND');
return promise;
}, 'postMessage in a service worker should be able to transfer ReadableStream');

done();
}

// Delay running the tests until we get a message from the page telling us to.
// This is to work around an issue where testharness.js doesn't detect
// completion of the tests if they fail too early.
onmessage = msg => {
if (msg.data === 'explicit trigger')
startTests();
};

</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';
importScripts('/resources/testharness.js', 'helpers.js');

onmessage = msg => {
const client = msg.source;
if (msg.data === 'SEND') {
sendingTest(client);
} else {
receivingTest(msg, client);
}
};

function sendingTest(client) {
const orig = createOriginalReadableStream();
try {
client.postMessage(orig, [orig]);
} catch (e) {
client.postMessage(e.message);
}
}

function receivingTest(msg, client) {
try {
msg.waitUntil(testMessage(msg)
.then(() => client.postMessage('OK'))
.catch(e => client.postMessage(`BAD: ${e}`)));
} catch (e) {
client.postMessage(`BAD: ${e}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
This is a testharness.js-based test.
PASS service-worker
FAIL serviceWorker.controller.postMessage should be able to transfer a ReadableStream Failed to execute 'postMessage' on 'ServiceWorker': Value at index 0 does not have a transferable type.
FAIL postMessage in a service worker should be able to transfer ReadableStream promise_test: Unhandled rejection with value: "Failed to execute 'postMessage' on 'Client': Value at index 0 does not have a transferable type."
Harness: the test ran to completion.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/serviceworker/resources/test-helpers.js"></script>
<script>
'use strict';

const kServiceWorkerUrl = 'resources/service-worker.js';
const kIframeUrl = 'resources/service-worker-iframe.html';

// A dummy test so that we can use the test-helpers.js functions
const test = async_test('service-worker');

function registerAndStart() {
return service_worker_unregister_and_register(
test, kServiceWorkerUrl, kIframeUrl)
.then(reg => wait_for_state(test, reg.installing, 'activated'))
.then(() => with_iframe(kIframeUrl, { auto_remove: true }))
.then(iframe => {
fetch_tests_from_window(iframe.contentWindow);
iframe.contentWindow.postMessage('explicit trigger', '*');
return service_worker_unregister_and_done(test, kIframeUrl);
});
}

onload = registerAndStart;

</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
This is a testharness.js-based test.
FAIL worker.postMessage should be able to transfer a ReadableStream Failed to execute 'postMessage' on 'MessagePort': Value at index 0 does not have a transferable type.
FAIL postMessage in a worker should be able to transfer a ReadableStream promise_test: Unhandled rejection with value: "Failed to execute 'postMessage' on 'MessagePort': Value at index 0 does not have a transferable type."
Harness: the test ran to completion.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script>
'use strict';

promise_test(t => {
const orig = createOriginalReadableStream();
const w = new SharedWorker('resources/receiving-shared-worker.js');
const promise = checkTestResults(w.port);
w.port.postMessage(orig, [orig]);
assert_true(orig.locked, 'the original stream should be locked');
return promise;
}, 'worker.postMessage should be able to transfer a ReadableStream');

promise_test(t => {
const w = new SharedWorker('resources/sending-shared-worker.js');
const promise = testMessageEventOrErrorMessage(w.port);
w.port.start();
return promise;
}, 'postMessage in a worker should be able to transfer a ReadableStream');

</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CONSOLE ERROR: line 43: Uncaught (in promise) Error: assert_array_equals: there should be no ports in the event lengths differ, expected 0 got 1
CONSOLE ERROR: line 43: Uncaught (in promise) Error: assert_array_equals: there should be no ports in the event lengths differ, expected 0 got 1
This is a testharness.js-based test.
Harness Error. harness_status.status = 1 , harness_status.message = assert_array_equals: there should be no ports in the event lengths differ, expected 0 got 1
FAIL window.postMessage should be able to transfer a ReadableStream Failed to execute 'postMessage' on 'Window': Value at index 0 does not have a transferable type.
FAIL port.postMessage should be able to transfer a ReadableStream Failed to execute 'postMessage' on 'MessagePort': Value at index 0 does not have a transferable type.
FAIL the same ReadableStream posted multiple times should arrive together Failed to execute 'postMessage' on 'Window': Value at index 0 does not have a transferable type.
FAIL transfer to and from an iframe should work promise_test: Unhandled rejection with value: object "TypeError: Failed to execute 'postMessage' on 'Window': Value at index 0 does not have a transferable type."
Harness: the test ran to completion.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script>
'use strict';

promise_test(t => {
const orig = createOriginalReadableStream();
const promise = testMessageEvent(window);
postMessage(orig, '*', [orig]);
assert_true(orig.locked, 'the original stream should be locked');
return promise;
}, 'window.postMessage should be able to transfer a ReadableStream');

promise_test(t => {
const orig = createOriginalReadableStream();
const promise = new Promise(resolve => {
window.addEventListener('message', msg => {
const port = msg.data;
resolve(testMessageEvent(port));
port.start();
}, {once: true});
});
const mc = new MessageChannel();
postMessage(mc.port1, '*', [mc.port1]);
mc.port2.postMessage(orig, [orig]);
mc.port2.close();
assert_true(orig.locked, 'the original stream should be locked');
return promise;
}, 'port.postMessage should be able to transfer a ReadableStream');

promise_test(t => {
const orig = createOriginalReadableStream();
const promise = new Promise(resolve => {
addEventListener('message', t.step_func(evt => {
const [rs1, rs2] = evt.data;
assert_equals(rs1, rs2, 'both ReadableStreams should be the same object');
resolve();
}), {once: true});
});
postMessage([orig, orig], '*', [orig]);
return promise;
}, 'the same ReadableStream posted multiple times should arrive together');

const onloadPromise = new Promise(resolve => onload = resolve);

promise_test(() => {
const orig = createOriginalReadableStream();
const promise = testMessageEvent(window);
return onloadPromise.then(() => {
const echoIframe = document.querySelector('#echo');
echoIframe.contentWindow.postMessage(orig, '*', [orig]);
return promise;
});
}, 'transfer to and from an iframe should work');
</script>

<iframe id=echo src="resources/echo-iframe.html" style="display:none"></iframe>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CONSOLE ERROR: line 5: Uncaught TypeError: Failed to execute 'postMessage' on 'DedicatedWorkerGlobalScope': Value at index 0 does not have a transferable type.
This is a testharness.js-based test.
FAIL worker.postMessage should be able to transfer a ReadableStream Failed to execute 'postMessage' on 'Worker': Value at index 0 does not have a transferable type.
FAIL postMessage in a worker should be able to transfer a ReadableStream promise_test: Unhandled rejection with value: "error in worker"
Harness: the test ran to completion.

Loading

0 comments on commit a1e223e

Please sign in to comment.