Skip to content

Commit

Permalink
feat: Add high-resolution timing function "fastly.now()" behind featu…
Browse files Browse the repository at this point in the history
…re flag "--enable-experimental-high-resolution-time-methods"

This function accepts no arguments and returns the number of Microseconds since the epoch, midnight, January 1, 1970 UTC.

This function is very useful to have for running performance tests within JavaScript applications.
  • Loading branch information
JakeChampion committed Apr 12, 2023
1 parent b1c4848 commit f090838
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 33 deletions.
36 changes: 25 additions & 11 deletions c-dependencies/js-compute-runtime/builtins/fastly.cpp
Expand Up @@ -130,13 +130,11 @@ bool Fastly::includeBytes(JSContext *cx, unsigned argc, JS::Value *vp) {
return true;
}

const JSFunctionSpec Fastly::methods[] = {
JS_FN("dump", dump, 1, 0),
JS_FN("enableDebugLogging", enableDebugLogging, 1, JSPROP_ENUMERATE),
JS_FN("getGeolocationForIpAddress", getGeolocationForIpAddress, 1, JSPROP_ENUMERATE),
JS_FN("getLogger", getLogger, 1, JSPROP_ENUMERATE),
JS_FN("includeBytes", includeBytes, 1, JSPROP_ENUMERATE),
JS_FS_END};
bool Fastly::now(JSContext *cx, unsigned argc, JS::Value *vp) {
JS::CallArgs args = CallArgsFromVp(argc, vp);
args.rval().setNumber(JS_Now());
return true;
}

bool Fastly::env_get(JSContext *cx, unsigned argc, JS::Value *vp) {
JS::CallArgs args = CallArgsFromVp(argc, vp);
Expand Down Expand Up @@ -204,19 +202,35 @@ const JSPropertySpec Fastly::properties[] = {
JSPROP_ENUMERATE),
JS_PS_END};

bool Fastly::create(JSContext *cx, JS::HandleObject global) {
bool Fastly::create(JSContext *cx, JS::HandleObject global, FastlyOptions options) {
JS::RootedObject fastly(cx, JS_NewPlainObject(cx));
if (!fastly)
if (!fastly) {
return false;
}

env.init(cx, Env::create(cx));
if (!env)
if (!env) {
return false;
}
baseURL.init(cx);
defaultBackend.init(cx);

if (!JS_DefineProperty(cx, global, "fastly", fastly, 0))
if (!JS_DefineProperty(cx, global, "fastly", fastly, 0)) {
return false;
}

JSFunctionSpec nowfn = JS_FN("now", now, 0, JSPROP_ENUMERATE);
JSFunctionSpec end = JS_FS_END;

const JSFunctionSpec methods[] = {
JS_FN("dump", dump, 1, 0),
JS_FN("enableDebugLogging", enableDebugLogging, 1, JSPROP_ENUMERATE),
JS_FN("getGeolocationForIpAddress", getGeolocationForIpAddress, 1, JSPROP_ENUMERATE),
JS_FN("getLogger", getLogger, 1, JSPROP_ENUMERATE),
JS_FN("includeBytes", includeBytes, 1, JSPROP_ENUMERATE),
options.getExperimentalHighResolutionTimeMethodsEnabled() ? nowfn : end,
end};

return JS_DefineFunctions(cx, fastly, methods) && JS_DefineProperties(cx, fastly, properties);
}

Expand Down
4 changes: 2 additions & 2 deletions c-dependencies/js-compute-runtime/builtins/fastly.h
Expand Up @@ -21,9 +21,9 @@ class Fastly : public BuiltinNoConstructor<Fastly> {
static JS::PersistentRooted<JSString *> defaultBackend;
static bool allowDynamicBackends;

static const JSFunctionSpec methods[];
static const JSPropertySpec properties[];

static bool now(JSContext *cx, unsigned argc, JS::Value *vp);
static bool dump(JSContext *cx, unsigned argc, JS::Value *vp);
static bool enableDebugLogging(JSContext *cx, unsigned argc, JS::Value *vp);
static bool getGeolocationForIpAddress(JSContext *cx, unsigned argc, JS::Value *vp);
Expand All @@ -36,7 +36,7 @@ class Fastly : public BuiltinNoConstructor<Fastly> {
static bool defaultBackend_set(JSContext *cx, unsigned argc, JS::Value *vp);
static bool allowDynamicBackends_get(JSContext *cx, unsigned argc, JS::Value *vp);
static bool allowDynamicBackends_set(JSContext *cx, unsigned argc, JS::Value *vp);
static bool create(JSContext *cx, JS::HandleObject global);
static bool create(JSContext *cx, JS::HandleObject global, FastlyOptions options);
};

} // namespace builtins
Expand Down
4 changes: 2 additions & 2 deletions c-dependencies/js-compute-runtime/js-compute-builtins.cpp
Expand Up @@ -1280,7 +1280,7 @@ bool math_random(JSContext *cx, unsigned argc, Value *vp) {
return true;
}

bool define_fastly_sys(JSContext *cx, HandleObject global) {
bool define_fastly_sys(JSContext *cx, HandleObject global, FastlyOptions options) {
// Allocating the reusable hostcall buffer here means it's baked into the
// snapshot, and since it's all zeros, it won't increase the size of the
// snapshot.
Expand All @@ -1292,7 +1292,7 @@ bool define_fastly_sys(JSContext *cx, HandleObject global) {

if (!builtins::Backend::init_class(cx, global))
return false;
if (!builtins::Fastly::create(cx, global))
if (!builtins::Fastly::create(cx, global, options))
return false;
if (!builtins::Console::create(cx, global))
return false;
Expand Down
23 changes: 22 additions & 1 deletion c-dependencies/js-compute-runtime/js-compute-builtins.h
Expand Up @@ -81,7 +81,28 @@ bool hasWizeningFinished();
bool isWizening();
void markWizeningAsFinished();

bool define_fastly_sys(JSContext *cx, JS::HandleObject global);
class FastlyOptions {
private:
uint8_t mask = 0;

public:
static constexpr const uint8_t experimental_high_resolution_time_methods_enabled_flag = 1 << 0;

FastlyOptions() = default;

bool getExperimentalHighResolutionTimeMethodsEnabled() {
return this->mask & experimental_high_resolution_time_methods_enabled_flag;
};
void setExperimentalHighResolutionTimeMethodsEnabled(bool set) {
if (set) {
this->mask |= experimental_high_resolution_time_methods_enabled_flag;
} else {
this->mask &= ~experimental_high_resolution_time_methods_enabled_flag;
}
};
};

bool define_fastly_sys(JSContext *cx, JS::HandleObject global, FastlyOptions options);

bool RejectPromiseWithPendingError(JSContext *cx, JS::HandleObject promise);

Expand Down
8 changes: 7 additions & 1 deletion c-dependencies/js-compute-runtime/js-compute-runtime.cpp
Expand Up @@ -358,7 +358,13 @@ void init() {
JSAutoRealm ar(cx, global);
FETCH_HANDLERS = new JS::PersistentRootedObjectVector(cx);

define_fastly_sys(cx, global);
bool ENABLE_EXPERIMENTAL_HIGH_RESOLUTION_TIME_METHODS =
std::string(std::getenv("ENABLE_EXPERIMENTAL_HIGH_RESOLUTION_TIME_METHODS")) == "1";
FastlyOptions options;
options.setExperimentalHighResolutionTimeMethodsEnabled(
ENABLE_EXPERIMENTAL_HIGH_RESOLUTION_TIME_METHODS);

define_fastly_sys(cx, global, options);
if (!JS_DefineFunction(cx, global, "addEventListener", addEventListener, 2, 0))
exit(1);

Expand Down
20 changes: 11 additions & 9 deletions integration-tests/cli/help.test.js
Expand Up @@ -19,19 +19,20 @@ test('--help should return help on stdout and zero exit code', async function (t
await cleanup();
});
const { code, stdout, stderr } = await execute(process.execPath, `${cli} --help`);

t.is(code, 0);
t.alike(stdout, [
`js-compute-runtime ${version}`,
'USAGE:',
'js-compute-runtime [FLAGS] [OPTIONS] [ARGS]',
'FLAGS:',
'-h, --help Prints help information',
'-V, --version Prints version information',
'-h, --help Prints help information',
'-V, --version Prints version information',
'OPTIONS:',
'--engine-wasm <engine-wasm> The JS engine Wasm file path',
'--engine-wasm <engine-wasm> The JS engine Wasm file path',
'--enable-experimental-high-resolution-time-methods Enable experimental high-resolution fastly.now() method',
'ARGS:',
'<input> The input JS script\'s file path [default: bin/index.js]',
"<input> The input JS script's file path [default: bin/index.js]",
'<output> The file path to write the output Wasm module to [default: bin/main.wasm]'
])
t.alike(stderr, [])
Expand All @@ -50,12 +51,13 @@ test('-h should return help on stdout and zero exit code', async function (t) {
'USAGE:',
'js-compute-runtime [FLAGS] [OPTIONS] [ARGS]',
'FLAGS:',
'-h, --help Prints help information',
'-V, --version Prints version information',
'-h, --help Prints help information',
'-V, --version Prints version information',
'OPTIONS:',
'--engine-wasm <engine-wasm> The JS engine Wasm file path',
'--engine-wasm <engine-wasm> The JS engine Wasm file path',
'--enable-experimental-high-resolution-time-methods Enable experimental high-resolution fastly.now() method',
'ARGS:',
'<input> The input JS script\'s file path [default: bin/index.js]',
"<input> The input JS script's file path [default: bin/index.js]",
'<output> The file path to write the output Wasm module to [default: bin/main.wasm]'
])
t.alike(stderr, [])
Expand Down
56 changes: 56 additions & 0 deletions integration-tests/js-compute/fixtures/fastly/bin/index.js
@@ -0,0 +1,56 @@
/* eslint-env serviceworker */

import { env } from 'fastly:env';
import { pass, fail, assert } from "../../../assertions.js";

addEventListener("fetch", event => {
event.respondWith(app(event))
})
/**
* @param {FetchEvent} event
* @returns {Response}
*/
async function app(event) {
try {
const path = (new URL(event.request.url)).pathname;
console.log(`path: ${path}`)
console.log(`FASTLY_SERVICE_VERSION: ${env('FASTLY_SERVICE_VERSION')}`)
if (routes.has(path)) {
const routeHandler = routes.get(path);
return await routeHandler()
}
return fail(`${path} endpoint does not exist`)
} catch (error) {
return fail(`The routeHandler threw an error: ${error.message}` + '\n' + error.stack)
}
}

const routes = new Map();
routes.set('/', () => {
routes.delete('/');
let test_routes = Array.from(routes.keys())
return new Response(JSON.stringify(test_routes), { 'headers': { 'content-type': 'application/json' } });
});
// fastly.now
{
routes.set("/fastly/now", function () {
let error = assert(typeof fastly.now, 'function', 'typeof fastly.now')
if (error) { return error }

error = assert(fastly.now.name, 'now', 'fastly.now.name')
if (error) { return error }

error = assert(fastly.now.length, 0, 'fastly.now.length')
if (error) { return error }

error = assert(typeof fastly.now(), 'number', `typeof fastly.now()`)
if (error) { return error }

error = assert(fastly.now() > Date.now(), true, `fastly.now() > Date.now()`)
if (error) { return error }

console.log(fastly.now())

return pass()
})
}
12 changes: 12 additions & 0 deletions integration-tests/js-compute/fixtures/fastly/fastly.toml.in
@@ -0,0 +1,12 @@
# This file describes a Fastly Compute@Edge package. To learn more visit:
# https://developer.fastly.com/reference/fastly-toml/

authors = ["me@jakechampion.name"]
description = ""
language = "other"
manifest_version = 2
name = "fastly"
service_id = ""

[scripts]
build = "node ../../../../js-compute-runtime-cli.js --enable-experimental-high-resolution-time-methods"
12 changes: 12 additions & 0 deletions integration-tests/js-compute/fixtures/fastly/tests.json
@@ -0,0 +1,12 @@
{
"GET /fastly/now": {
"environments": ["c@e", "viceroy"],
"downstream_request": {
"method": "GET",
"pathname": "/fastly/now"
},
"downstream_response": {
"status": 200
}
}
}
12 changes: 10 additions & 2 deletions js-compute-runtime-cli.js
Expand Up @@ -5,7 +5,15 @@ import { printVersion } from "./src/printVersion.js";
import { printHelp } from "./src/printHelp.js";
import { addSdkMetadataField } from "./src/addSdkMetadataField.js";

const {wasmEngine, input, component, output, version, help} = await parseInputs(process.argv.slice(2))
const {
enableExperimentalHighResolutionTimeMethods,
wasmEngine,
input,
component,
output,
version,
help
} = await parseInputs(process.argv.slice(2))

if (version) {
await printVersion();
Expand All @@ -19,7 +27,7 @@ if (version) {
// it could be that the user is using an older version of js-compute-runtime
// and a newer version does support the platform they are using.
const {compileApplicationToWasm} = await import('./src/compileApplicationToWasm.js')
await compileApplicationToWasm(input, output, wasmEngine);
await compileApplicationToWasm(input, output, wasmEngine, enableExperimentalHighResolutionTimeMethods);
if (component) {
const {compileComponent} = await import('./src/component.js');
await compileComponent(output);
Expand Down
6 changes: 5 additions & 1 deletion src/compileApplicationToWasm.js
Expand Up @@ -8,7 +8,7 @@ import { precompile } from "./precompile.js";
import { bundle } from "./bundle.js";
import { containsSyntaxErrors } from "./containsSyntaxErrors.js";

export async function compileApplicationToWasm(input, output, wasmEngine) {
export async function compileApplicationToWasm(input, output, wasmEngine, enableExperimentalHighResolutionTimeMethods = false) {
try {
if (!(await isFile(input))) {
console.error(
Expand Down Expand Up @@ -85,6 +85,7 @@ export async function compileApplicationToWasm(input, output, wasmEngine) {
let wizerProcess = spawnSync(
wizer,
[
"--inherit-env=true",
"--allow-wasi",
`--dir=.`,
`--wasm-bulk-memory=true`,
Expand All @@ -97,6 +98,9 @@ export async function compileApplicationToWasm(input, output, wasmEngine) {
input: application,
shell: true,
encoding: "utf-8",
env: {
ENABLE_EXPERIMENTAL_HIGH_RESOLUTION_TIME_METHODS: enableExperimentalHighResolutionTimeMethods ? '1' : '0'
}
}
);
if (wizerProcess.status !== 0) {
Expand Down
7 changes: 6 additions & 1 deletion src/parseInputs.js
Expand Up @@ -7,6 +7,7 @@ export async function parseInputs(cliInputs) {
const __dirname = dirname(fileURLToPath(import.meta.url));

let component = false;
let enableExperimentalHighResolutionTimeMethods = false;
let customEngineSet = false;
let wasmEngine = join(__dirname, "../js-compute-runtime.wasm");
let customInputSet = false;
Expand All @@ -20,6 +21,10 @@ export async function parseInputs(cliInputs) {
case "--": {
break loop;
}
case "--enable-experimental-high-resolution-time-methods": {
enableExperimentalHighResolutionTimeMethods = true;
break;
}
case "-V":
case "--version": {
return { version: true };
Expand Down Expand Up @@ -88,5 +93,5 @@ export async function parseInputs(cliInputs) {
}
}
}
return { wasmEngine, component, input, output };
return { wasmEngine, component, input, output, enableExperimentalHighResolutionTimeMethods };
}
7 changes: 4 additions & 3 deletions src/printHelp.js
Expand Up @@ -7,11 +7,12 @@ USAGE:
js-compute-runtime [FLAGS] [OPTIONS] [ARGS]
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
--engine-wasm <engine-wasm> The JS engine Wasm file path
--engine-wasm <engine-wasm> The JS engine Wasm file path
--enable-experimental-high-resolution-time-methods Enable experimental high-resolution fastly.now() method
ARGS:
<input> The input JS script's file path [default: bin/index.js]
Expand Down

0 comments on commit f090838

Please sign in to comment.