diff --git a/JetStreamDriver.js b/JetStreamDriver.js index 92df598e..b425f50e 100644 --- a/JetStreamDriver.js +++ b/JetStreamDriver.js @@ -1218,7 +1218,7 @@ class AsyncBenchmark extends DefaultBenchmark { get runnerCode() { return ` async function doRun() { - let __benchmark = new Benchmark(); + let __benchmark = new Benchmark(${this.iterations}); await __benchmark.init?.(); let results = []; let benchmarkName = "${this.name}"; @@ -1923,6 +1923,20 @@ let BENCHMARKS = [ ], tags: ["Default", "Proxy"], }), + new AsyncBenchmark({ + name: "web-ssr", + files: [ + "./web-ssr/benchmark.js", + ], + preload: { + // Debug Sources for nicer profiling. + // sourceCodeBlob: "./web-ssr/dist/react-render-test.js", + REACT_RENDER_TEST_BLOB: "./web-ssr/dist/react-render-test.minified.js", + }, + tags: ["Default", "web", "ssr"], + iterations: 5, + worstCaseCount: 3, + }), // Class fields new DefaultBenchmark({ name: "raytrace-public-class-fields", diff --git a/web-ssr/babel.config.json b/web-ssr/babel.config.json new file mode 100644 index 00000000..2b7bafa5 --- /dev/null +++ b/web-ssr/babel.config.json @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react"] +} diff --git a/web-ssr/benchmark-node.cjs b/web-ssr/benchmark-node.cjs new file mode 100644 index 00000000..a884e393 --- /dev/null +++ b/web-ssr/benchmark-node.cjs @@ -0,0 +1,14 @@ +// node standalone version of the benchmark for local testing. + +const {renderTest } = require("./src/react-render-test.cjs") + +console.log("Starting TypeScript in-memory compilation benchmark..."); +const startTime = performance.now(); + +renderTest(); + +const endTime = performance.now(); +const duration = (endTime - startTime) / 1000; + +console.log(`TypeScript compilation finished.`); +console.log(`Compilation took ${duration.toFixed(2)} seconds.`); \ No newline at end of file diff --git a/web-ssr/benchmark.js b/web-ssr/benchmark.js new file mode 100644 index 00000000..3f772428 --- /dev/null +++ b/web-ssr/benchmark.js @@ -0,0 +1,91 @@ +globalThis.console = { + log() { }, + warn() { }, + assert(condition) { + if (!condition) throw new Error("Invalid assertion"); + } +}; + +globalThis.clearTimeout = function () { }; + + +function quickHash(str) { + let hash = 5381; + let i = str.length; + while (i > 0) { + hash = (hash * 33) ^ (str.charCodeAt(i) | 0); + i-= 919; + } + return hash | 0; +} + +const CACHE_BUST_COMMENT = "/*ThouShaltNotCache*/"; +const CACHE_BUST_COMMENT_RE = new RegExp(`\n${RegExp.escape(CACHE_BUST_COMMENT)}\n`, "g"); + +// JetStream benchmark. +class Benchmark { + measureStartup = true; + iterationCount = 0; + iteration = 0; + lastResult = {}; + sourceCode; + sourceHash = 0 + iterationSourceCodes = []; + + constructor(iterationCount) { + this.iterationCount = iterationCount + } + + async init() { + this.sourceCode = await getString(REACT_RENDER_TEST_BLOB); + this.expect("Cache Comment Count", this.sourceCode.match(CACHE_BUST_COMMENT_RE).length, 597); + // Warm up the hash function. + this.sourceHash = quickHash(this.sourceCode); + for (let i = 0; i < this.iterationCount; i++) + this.iterationSourceCodes[i] = this.prepareCode(i); + } + + + prepareCode(iteration) { + if (!this.measureStartup) + return this.sourceCode; + // Alter the code per iteration to prevent caching. + const iterationId = `${String.fromCharCode(97 + (iteration % 25))}${iteration}`; + const sourceCode = this.sourceCode.replaceAll(CACHE_BUST_COMMENT_RE, `/*${iterationId}*/`); + return sourceCode; + } + + runIteration() { + let sourceCode = this.iterationSourceCodes[this.iteration]; + if (!sourceCode) + throw new Error(`Could not find source for iteration ${this.iteration}`); + // Module in sourceCode it assigned to the ReactRenderTest variable. + let ReactRenderTest; + + let initStart = performance.now(); + const res = eval(sourceCode); + const runStart = performance.now(); + + this.lastResult = ReactRenderTest.renderTest(); + this.lastResult.htmlHash = quickHash(this.lastResult.html); + const end = performance.now(); + + const loadTime = runStart - initStart; + const runTime = end - runStart; + // For local debugging: + // print(`Iteration ${this.iteration}:`); + // print(` Load time: ${loadTime.toFixed(2)}ms`); + // print(` Render time: ${runTime.toFixed(2)}ms`); + this.iteration++; + } + + validate() { + this.expect("HTML length", this.lastResult.html.length, 183778); + this.expect("HTML hash", this.lastResult.htmlHash, 1177839858); + } + + expect(name, value, expected) { + if (value != expected) + throw new Error(`Expected ${name} to be ${expected}, but got ${value}`); + } +} diff --git a/web-ssr/build/cache-buster-comment-plugin.cjs b/web-ssr/build/cache-buster-comment-plugin.cjs new file mode 100644 index 00000000..63f6ffbf --- /dev/null +++ b/web-ssr/build/cache-buster-comment-plugin.cjs @@ -0,0 +1,29 @@ +// Babel plugin that adds CACHE_BUST_COMMENT to every function body. +const CACHE_BUST_COMMENT = "ThouShaltNotCache"; + + +module.exports = function({ types: t }) { + return { + visitor: { + Function(path) { + const bodyPath = path.get("body"); + // Handle arrow functions: () => "value" + // Convert them to block statements: () => { return "value"; } + if (!bodyPath.isBlockStatement()) { + const newBody = t.blockStatement([t.returnStatement(bodyPath.node)]); + path.set("body", newBody); + } + + // Handle empty function bodies: function foo() {} + // Add an empty statement so we have a first node to attach the comment to. + if (path.get("body.body").length === 0) { + path.get("body").pushContainer("body", t.emptyStatement()); + } + + const firstNode = path.node.body.body[0]; + t.addComment(firstNode, "leading", CACHE_BUST_COMMENT); + + } + }, + }; +}; diff --git a/web-ssr/dist/react-render-test.js b/web-ssr/dist/react-render-test.js new file mode 100644 index 00000000..9724182a --- /dev/null +++ b/web-ssr/dist/react-render-test.js @@ -0,0 +1,1221 @@ +/*! For license information please see react-render-test.js.LICENSE.txt */ +(()=>{var __webpack_modules__={57:(module,__unused_webpack_exports,__webpack_require__)=>{"use strict";module.exports=__webpack_require__(158)},96:(__unused_webpack_module,exports,__webpack_require__)=>{"use strict";var MessageChannel=__webpack_require__(492).MessageChannel,TextEncoder=__webpack_require__(997).TextEncoder;function _typeof(o){/*ThouShaltNotCache*/return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(o){/*ThouShaltNotCache*/return typeof o}:function(o){/*ThouShaltNotCache*/return o&&"function"==typeof Symbol&&o.constructor===Symbol&&o!==Symbol.prototype?"symbol":typeof o},_typeof(o)}var React=__webpack_require__(57),ReactDOM=__webpack_require__(520);function formatProdErrorMessage(code){ +/*ThouShaltNotCache*/ +var url="https://react.dev/errors/"+code;if(1>>16)&65535)<<16)&4294967295)<<15|k1>>>17))+((461845907*(k1>>>16)&65535)<<16)&4294967295)<<13|h1>>>19))+((5*(h1>>>16)&65535)<<16)&4294967295))+(((h1>>>16)+58964&65535)<<16)}switch(k1=0,remainder){case 3:k1^=(255&key.charCodeAt(seed+2))<<16;case 2:k1^=(255&key.charCodeAt(seed+1))<<8;case 1:h1^=461845907*(65535&(k1=(k1=3432918353*(65535&(k1^=255&key.charCodeAt(seed)))+((3432918353*(k1>>>16)&65535)<<16)&4294967295)<<15|k1>>>17))+((461845907*(k1>>>16)&65535)<<16)&4294967295}return h1^=key.length,h1=2246822507*(65535&(h1^=h1>>>16))+((2246822507*(h1>>>16)&65535)<<16)&4294967295,((h1=3266489909*(65535&(h1^=h1>>>13))+((3266489909*(h1>>>16)&65535)<<16)&4294967295)^h1>>>16)>>>0}var channel=new MessageChannel,taskQueue=[];function scheduleWork(callback){ +/*ThouShaltNotCache*/ +taskQueue.push(callback),channel.port2.postMessage(null)}function handleErrorInNextTick(error){ +/*ThouShaltNotCache*/ +setTimeout(function(){ +/*ThouShaltNotCache*/ +throw error})}channel.port1.onmessage=function(){ +/*ThouShaltNotCache*/ +var task=taskQueue.shift();task&&task()};var LocalPromise=Promise,scheduleMicrotask="function"==typeof queueMicrotask?queueMicrotask:function(callback){ +/*ThouShaltNotCache*/ +LocalPromise.resolve(null).then(callback).catch(handleErrorInNextTick)},currentView=null,writtenBytes=0;function writeChunk(destination,chunk){ +/*ThouShaltNotCache*/ +if(0!==chunk.byteLength)if(2048]/;function escapeTextForBrowser(text){ +/*ThouShaltNotCache*/ +if("boolean"==typeof text||"number"==typeof text||"bigint"==typeof text)return""+text;text=""+text;var match=matchHtmlRegExp.exec(text);if(match){var index,html="",lastIndex=0;for(index=match.index;index; rel=dns-prefetch",JSCompiler_temp=0<=(resumableState.remainingCapacity-=header.length+2)),JSCompiler_temp?(renderState.resets.dns[href]=null,resumableState.preconnects&&(resumableState.preconnects+=", "),resumableState.preconnects+=header):(pushLinkImpl(header=[],{href,rel:"dns-prefetch"}),renderState.preconnects.add(header));enqueueFlush(request)}}else previousDispatcher.D(href)},C:function(href,crossOrigin){ +/*ThouShaltNotCache*/ +var request=currentRequest||null;if(request){var resumableState=request.resumableState,renderState=request.renderState;if("string"==typeof href&&href){var bucket="use-credentials"===crossOrigin?"credentials":"string"==typeof crossOrigin?"anonymous":"default";if(!resumableState.connectResources[bucket].hasOwnProperty(href)){var header,JSCompiler_temp;if(resumableState.connectResources[bucket][href]=null,JSCompiler_temp=(resumableState=renderState.headers)&&0; rel=preconnect","string"==typeof crossOrigin)JSCompiler_temp+='; crossorigin="'+(""+crossOrigin).replace(regexForLinkHeaderQuotedParamValueContext,escapeStringForLinkHeaderQuotedParamValueContextReplacer)+'"';header=JSCompiler_temp,JSCompiler_temp=0<=(resumableState.remainingCapacity-=header.length+2)}JSCompiler_temp?(renderState.resets.connect[bucket][href]=null,resumableState.preconnects&&(resumableState.preconnects+=", "),resumableState.preconnects+=header):(pushLinkImpl(bucket=[],{rel:"preconnect",href,crossOrigin}),renderState.preconnects.add(bucket))}enqueueFlush(request)}}else previousDispatcher.C(href,crossOrigin)},L:function(href,as,options){ +/*ThouShaltNotCache*/ +var request=currentRequest||null;if(request){var resumableState=request.resumableState,renderState=request.renderState;if(as&&href){switch(as){case"image":if(options)var imageSrcSet=options.imageSrcSet,imageSizes=options.imageSizes,fetchPriority=options.fetchPriority;var header,key=imageSrcSet?imageSrcSet+"\n"+(imageSizes||""):href;if(resumableState.imageResources.hasOwnProperty(key))return;resumableState.imageResources[key]=PRELOAD_NO_CREDS,(resumableState=renderState.headers)&&0');var startInlineScript=stringToPrecomputedChunk("