diff --git a/BUILD.gn b/BUILD.gn index a1ffc750f1..038e70430d 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -80,6 +80,7 @@ ts_sources = [ "js/symlink.ts", "js/text_encoding.ts", "js/timers.ts", + "js/trace.ts", "js/types.ts", "js/util.ts", "js/v8_source_maps.ts", diff --git a/js/deno.ts b/js/deno.ts index 9be05e9167..b14bcc5618 100644 --- a/js/deno.ts +++ b/js/deno.ts @@ -13,4 +13,5 @@ export { writeFileSync, writeFile } from "./write_file"; export { ErrorKind, DenoError } from "./errors"; export { libdeno } from "./libdeno"; export { arch, platform } from "./platform"; +export { trace } from "./trace"; export const argv: string[] = []; diff --git a/js/dispatch.ts b/js/dispatch.ts index 8c2514c32b..037b77a854 100644 --- a/js/dispatch.ts +++ b/js/dispatch.ts @@ -4,6 +4,7 @@ import { flatbuffers } from "flatbuffers"; import * as fbs from "gen/msg_generated"; import * as errors from "./errors"; import * as util from "./util"; +import { maybePushTrace } from "./trace"; let nextCmdId = 0; const promiseTable = new Map>(); @@ -29,6 +30,7 @@ export function sendAsync( msgType: fbs.Any, msg: flatbuffers.Offset ): Promise { + maybePushTrace(msgType, false); // add to trace if tracing const [cmdId, resBuf] = sendInternal(builder, msgType, msg, false); util.assert(resBuf == null); const promise = util.createResolvable(); @@ -42,6 +44,7 @@ export function sendSync( msgType: fbs.Any, msg: flatbuffers.Offset ): null | fbs.Base { + maybePushTrace(msgType, true); // add to trace if tracing const [cmdId, resBuf] = sendInternal(builder, msgType, msg, true); util.assert(cmdId >= 0); if (resBuf == null) { diff --git a/js/trace.ts b/js/trace.ts new file mode 100644 index 0000000000..42a9fe0159 --- /dev/null +++ b/js/trace.ts @@ -0,0 +1,80 @@ +// Copyright 2018 the Deno authors. All rights reserved. MIT license. +import * as fbs from "gen/msg_generated"; + +export interface TraceInfo { + sync: boolean; // is synchronous call + name: string; // name of operation +} + +interface TraceStackNode { + list: TraceInfo[]; + prev: TraceStackNode | null; +} + +let current: TraceStackNode | null = null; + +// Push a new list to trace stack +function pushStack(): void { + if (current === null) { + current = { list: [], prev: null }; + } else { + const newStack = { list: [], prev: current }; + current = newStack; + } +} + +// Pop from trace stack and (if possible) concat to parent trace stack node +function popStack(): TraceInfo[] { + if (current === null) { + throw new Error("trace list stack should not be empty"); + } + const resultList = current!.list; + if (!!current!.prev) { + const prev = current!.prev!; + // concat inner results to outer stack + prev.list = prev.list.concat(resultList); + current = prev; + } else { + current = null; + } + return resultList; +} + +// Push to trace stack if we are tracing +export function maybePushTrace(op: fbs.Any, sync: boolean): void { + if (current === null) { + return; // no trace requested + } + // Freeze the object, avoid tampering + current!.list.push( + Object.freeze({ + sync, + name: fbs.Any[op] // convert to enum names + }) + ); +} + +/** + * Trace operations executed inside a given function or promise. + * Notice: To capture every operation in asynchronous deno.* calls, + * you might want to put them in functions instead of directly invoking. + * + * import { trace, mkdir } from "deno"; + * + * const ops = await trace(async () => { + * await mkdir("my_dir"); + * }); + * // ops becomes [{ sync: false, name: "Mkdir" }] + */ +export async function trace( + // tslint:disable-next-line:no-any + fnOrPromise: Function | Promise +): Promise { + pushStack(); + if (typeof fnOrPromise === "function") { + await fnOrPromise(); + } else { + await fnOrPromise; + } + return popStack(); +} diff --git a/js/trace_test.ts b/js/trace_test.ts new file mode 100644 index 0000000000..ac83c1064b --- /dev/null +++ b/js/trace_test.ts @@ -0,0 +1,81 @@ +import { testPerm, assertEqual } from "./test_util.ts"; +import * as deno from "deno"; + +testPerm({ write: true }, async function traceFunctionSuccess() { + const op = await deno.trace(async () => { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + // Mixing sync and async calls + const filename = deno.makeTempDirSync() + "/test.txt"; + await deno.writeFile(filename, data, 0o666); + await deno.removeSync(filename); + }); + assertEqual(op.length, 3); + assertEqual(op[0], { sync: true, name: "MakeTempDir" }); + assertEqual(op[1], { sync: false, name: "WriteFile" }); + assertEqual(op[2], { sync: true, name: "Remove" }); +}); + +testPerm({ write: true }, async function tracePromiseSuccess() { + // Ensure we don't miss any send actions + // (new Promise(fn), fn runs synchronously) + const asyncFunction = async () => { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + // Mixing sync and async calls + const filename = deno.makeTempDirSync() + "/test.txt"; + await deno.writeFile(filename, data, 0o666); + await deno.removeSync(filename); + }; + const promise = Promise.resolve().then(asyncFunction); + const op = await deno.trace(promise); + assertEqual(op.length, 3); + assertEqual(op[0], { sync: true, name: "MakeTempDir" }); + assertEqual(op[1], { sync: false, name: "WriteFile" }); + assertEqual(op[2], { sync: true, name: "Remove" }); +}); + +testPerm({ write: true }, async function traceRepeatSuccess() { + const op1 = await deno.trace(async () => await deno.makeTempDir()); + assertEqual(op1.length, 1); + assertEqual(op1[0], { sync: false, name: "MakeTempDir" }); + const op2 = await deno.trace(async () => await deno.statSync(".")); + assertEqual(op2.length, 1); + assertEqual(op2[0], { sync: true, name: "Stat" }); +}); + +testPerm({ write: true }, async function traceIdempotence() { + let op1, op2, op3; + op1 = await deno.trace(async () => { + const filename = (await deno.makeTempDir()) + "/test.txt"; + op2 = await deno.trace(async () => { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + deno.writeFileSync(filename, data, 0o666); + op3 = await deno.trace(async () => { + await deno.remove(filename); + }); + await deno.makeTempDir(); + }); + }); + + // Flatten the calls + assertEqual(op1.length, 4); + assertEqual(op1[0], { sync: false, name: "MakeTempDir" }); + assertEqual(op1[1], { sync: true, name: "WriteFile" }); + assertEqual(op1[2], { sync: false, name: "Remove" }); + assertEqual(op1[3], { sync: false, name: "MakeTempDir" }); + + assertEqual(op2.length, 3); + assertEqual(op2[0], { sync: true, name: "WriteFile" }); + assertEqual(op2[1], { sync: false, name: "Remove" }); + assertEqual(op2[2], { sync: false, name: "MakeTempDir" }); + + assertEqual(op3.length, 1); + assertEqual(op3[0], { sync: false, name: "Remove" }); + + // Expect top-level repeat still works after all the nestings + const op4 = await deno.trace(async () => await deno.statSync(".")); + assertEqual(op4.length, 1); + assertEqual(op4[0], { sync: true, name: "Stat" }); +}); diff --git a/js/unit_tests.ts b/js/unit_tests.ts index 9b85cf3ec0..5c32f710fd 100644 --- a/js/unit_tests.ts +++ b/js/unit_tests.ts @@ -16,3 +16,4 @@ import "./timers_test.ts"; import "./symlink_test.ts"; import "./platform_test.ts"; import "./text_encoding_test.ts"; +import "./trace_test.ts";