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

refactor(minimal ops) refactor minimal ops & rename to buffer ops #9719

Merged
merged 8 commits into from
Mar 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions cli/tests/unit/dispatch_buffer_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
assert,
assertEquals,
assertMatch,
unitTest,
unreachable,
} from "./test_util.ts";

const readErrorStackPattern = new RegExp(
`^.*
at handleError \\(.*10_dispatch_buffer\\.js:.*\\)
at bufferOpParseResult \\(.*10_dispatch_buffer\\.js:.*\\)
at Array.<anonymous> \\(.*10_dispatch_buffer\\.js:.*\\).*$`,
"ms",
);

unitTest(async function sendAsyncStackTrace(): Promise<void> {
const buf = new Uint8Array(10);
const rid = 10;
try {
await Deno.read(rid, buf);
unreachable();
} catch (error) {
assertMatch(error.stack, readErrorStackPattern);
}
});

declare global {
// deno-lint-ignore no-namespace
namespace Deno {
// deno-lint-ignore no-explicit-any
var core: any; // eslint-disable-line no-var
}
}

unitTest(function bufferOpsHeaderTooShort(): void {
for (const op of ["op_read_sync", "op_read_async"]) {
const readOpId = Deno.core.ops()[op];
const res = Deno.core.send(
readOpId,
new Uint8Array([
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
]),
);

const headerByteLength = 4 * 4;
assert(res.byteLength > headerByteLength);
const view = new DataView(
res.buffer,
res.byteOffset + res.byteLength - headerByteLength,
headerByteLength,
);

const requestId = Number(view.getBigUint64(0, true));
const status = view.getUint32(8, true);
const result = view.getUint32(12, true);

assert(requestId === 0);
assert(status !== 0);
assertEquals(new TextDecoder().decode(res.slice(0, result)), "TypeError");
assertEquals(
new TextDecoder().decode(res.slice(result, -headerByteLength)).trim(),
"Unparsable control buffer",
);
}
});
49 changes: 0 additions & 49 deletions cli/tests/unit/dispatch_minimal_test.ts

This file was deleted.

4 changes: 2 additions & 2 deletions cli/tests/unit/metrics_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ unitTest(async function metrics(): Promise<void> {
assert(m1.bytesSentControl > 0);
assert(m1.bytesSentData >= 0);
assert(m1.bytesReceived > 0);
const m1OpWrite = m1.ops["op_write"];
const m1OpWrite = m1.ops["op_write_async"];
assert(m1OpWrite.opsDispatchedAsync > 0);
assert(m1OpWrite.opsCompletedAsync > 0);
assert(m1OpWrite.bytesSentControl > 0);
Expand All @@ -28,7 +28,7 @@ unitTest(async function metrics(): Promise<void> {
assert(m2.bytesSentControl > m1.bytesSentControl);
assert(m2.bytesSentData >= m1.bytesSentData + dataMsg.byteLength);
assert(m2.bytesReceived > m1.bytesReceived);
const m2OpWrite = m2.ops["op_write"];
const m2OpWrite = m2.ops["op_write_async"];
assert(m2OpWrite.opsDispatchedAsync > m1OpWrite.opsDispatchedAsync);
assert(m2OpWrite.opsCompletedAsync > m1OpWrite.opsCompletedAsync);
assert(m2OpWrite.bytesSentControl > m1OpWrite.bytesSentControl);
Expand Down
2 changes: 1 addition & 1 deletion cli/tests/unit/unit_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import "./console_test.ts";
import "./copy_file_test.ts";
import "./custom_event_test.ts";
import "./dir_test.ts";
import "./dispatch_minimal_test.ts";
import "./dispatch_buffer_test.ts";
import "./dispatch_json_test.ts";
import "./error_stack_test.ts";
import "./event_test.ts";
Expand Down
5 changes: 5 additions & 0 deletions core/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ SharedQueue Binary Layout
asyncHandlers[opId] = cb;
}

function setAsyncHandlerByName(opName, cb) {
setAsyncHandler(opsCache[opName], cb);
}
bartlomieju marked this conversation as resolved.
Show resolved Hide resolved

function handleAsyncMsgFromRust() {
while (true) {
const opIdBuf = shift();
Expand Down Expand Up @@ -256,6 +260,7 @@ SharedQueue Binary Layout
jsonOpAsync,
jsonOpSync,
setAsyncHandler,
setAsyncHandlerByName,
dispatch: send,
dispatchByName: dispatch,
ops,
Expand Down
150 changes: 150 additions & 0 deletions runtime/js/10_dispatch_buffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
"use strict";

((window) => {
const core = window.Deno.core;

function assert(cond) {
if (!cond) {
throw Error("assert");
}
}

////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////// General async handling //////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////

// General Async response handling
let nextRequestId = 1;
const promiseTable = {};

function opAsync(opName, opRequestBuilder, opResultParser) {
// Make sure requests of this type are handled by the asyncHandler
// The asyncHandler's role is to call the "promiseTable[requestId]" function
core.setAsyncHandlerByName(opName, (bufUi8, _) => {
Copy link
Member

Choose a reason for hiding this comment

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

Not entirely sure, but I have a feeling that using arrow function here might be less performant than having a named function definition before opAsync

Copy link
Contributor Author

Choose a reason for hiding this comment

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

opResultParser is passed into this arrow function, so that makes it hard to extract this arrow function from this opAsync function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

a solution could be to call this function on an object that stores this opResultParser function, but I found this quickly makes the code a lot less readable. please let me know if you think this is useful (I could create such a solution and share it with you for review)

Copy link
Member

Choose a reason for hiding this comment

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

@inteon is it required to pass that opResultParser? I think you could just directly call it in such function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

I see, let's land it as is then.

Copy link
Member

Choose a reason for hiding this comment

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

Also wondering if this should be called only once, otherwise a new callback is set with each call to the op

Copy link
Contributor Author

@inteon inteon Mar 16, 2021

Choose a reason for hiding this comment

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

I also think this should be called once. The current jsonOp implementation also does this wrongly (so it is somewhat unrelated to this PR):

deno/core/core.js

Lines 214 to 217 in bd961c3

async function jsonOpAsync(opName, args = null, ...zeroCopy) {
setAsyncHandler(opsCache[opName], jsonOpAsyncHandler);

I do fix this for both bufferOps and jsonOps in #9457.

const [requestId, result, error] = opResultParser(bufUi8, true);
inteon marked this conversation as resolved.
Show resolved Hide resolved
if (error !== null) {
promiseTable[requestId][1](error);
} else {
promiseTable[requestId][0](result);
}
delete promiseTable[requestId];
});

const requestId = nextRequestId++;

// Create and store promise
const promise = new Promise((resolve, reject) => {
promiseTable[requestId] = [resolve, reject];
});

// Synchronously dispatch async request
core.dispatchByName(opName, ...opRequestBuilder(requestId));

// Wait for async response
return promise;
}

function opSync(opName, opRequestBuilder, opResultParser) {
const rawResult = core.dispatchByName(opName, ...opRequestBuilder());

const [_, result, error] = opResultParser(rawResult, false);
if (error !== null) throw error;
return result;
}

////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////// Error handling /////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////

function handleError(className, message) {
const [ErrorClass, args] = core.getErrorClassAndArgs(className);
if (!ErrorClass) {
return new Error(
`Unregistered error class: "${className}"\n` +
` ${message}\n` +
` Classes of errors returned from ops should be registered via Deno.core.registerErrorClass().`,
);
}
return new ErrorClass(message, ...args);
}

////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////// Buffer ops handling //////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////

const scratchBytes = new ArrayBuffer(3 * 4);
const scratchView = new DataView(
scratchBytes,
scratchBytes.byteOffset,
scratchBytes.byteLength,
);

function bufferOpBuildRequest(requestId, argument, zeroCopy) {
scratchView.setBigUint64(0, BigInt(requestId), true);
scratchView.setUint32(8, argument, true);
return [scratchView, ...zeroCopy];
}

function bufferOpParseResult(bufUi8, isCopyNeeded) {
// Decode header value from ui8 buffer
const headerByteLength = 4 * 4;
assert(bufUi8.byteLength >= headerByteLength);
assert(bufUi8.byteLength % 4 == 0);
const view = new DataView(
bufUi8.buffer,
bufUi8.byteOffset + bufUi8.byteLength - headerByteLength,
headerByteLength,
);

const requestId = Number(view.getBigUint64(0, true));
const status = view.getUint32(8, true);
const result = view.getUint32(12, true);

// Error handling
if (status !== 0) {
const className = core.decode(bufUi8.subarray(0, result));
const message = core.decode(bufUi8.subarray(result, -headerByteLength))
.trim();

return [requestId, null, handleError(className, message)];
}

if (bufUi8.byteLength === headerByteLength) {
return [requestId, result, null];
}

// Rest of response buffer is passed as reference or as a copy
let respBuffer = null;
if (isCopyNeeded) {
// Copy part of the response array (if sent through shared array buf)
respBuffer = bufUi8.slice(0, result);
} else {
// Create view on existing array (if sent through overflow)
respBuffer = bufUi8.subarray(0, result);
}

return [requestId, respBuffer, null];
}

function bufferOpAsync(opName, argument = 0, ...zeroCopy) {
return opAsync(
opName,
(requestId) => bufferOpBuildRequest(requestId, argument, zeroCopy),
bufferOpParseResult,
);
}

function bufferOpSync(opName, argument = 0, ...zeroCopy) {
return opSync(
opName,
() => bufferOpBuildRequest(0, argument, zeroCopy),
bufferOpParseResult,
);
}

window.__bootstrap.dispatchBuffer = {
bufferOpSync,
bufferOpAsync,
};
})(this);
Loading