diff --git a/JetStreamDriver.js b/JetStreamDriver.js index 0ef099ba..a655a710 100644 --- a/JetStreamDriver.js +++ b/JetStreamDriver.js @@ -2026,6 +2026,19 @@ let BENCHMARKS = [ ], tags: ["Default", "Proxy"], }), + new AsyncBenchmark({ + name: "web-ssr", + files: [ + "./web-ssr/benchmark.js", + ], + preload: { + // Debug Sources for nicer profiling. + // BUNDLE_BLOB: "./web-ssr/dist/bundle.js", + BUNDLE_BLOB: "./web-ssr/dist/bundle.min.js", + }, + tags: ["Default", "web", "ssr"], + iterations: 30, + }), // Class fields new DefaultBenchmark({ name: "raytrace-public-class-fields", diff --git a/package-lock.json b/package-lock.json index 86b557e2..67e4f779 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "jetstream", "version": "3.0.0-alpha", "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@dapplets/unicode-escape-webpack-plugin": "^0.1.1" + }, "devDependencies": { "@actions/core": "^1.11.1", "@babel/core": "^7.21.3", @@ -470,6 +473,12 @@ "integrity": "sha512-1uLNT5NZsUVIGS4syuHwTzZ8HycMPyr6POA3FCE4GbMtc4rhoJk8aZKtNIRthJYfL+iioppi+rTfH3olMPr9nA==", "dev": true }, + "node_modules/@dapplets/unicode-escape-webpack-plugin": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@dapplets/unicode-escape-webpack-plugin/-/unicode-escape-webpack-plugin-0.1.1.tgz", + "integrity": "sha512-dBcrCWE6ZOOHD1XLe3raXk29CwQSJQCXYx8QNcCMvfG+cZ9tZh1h2OVV7D936pmues8OTm1KhjWeiZXbH30SAg==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", diff --git a/package.json b/package.json index bebd87c1..d6a1ec26 100644 --- a/package.json +++ b/package.json @@ -42,5 +42,8 @@ "local-web-server": "^5.4.0", "prettier": "^2.8.3", "selenium-webdriver": "^4.35.0" + }, + "dependencies": { + "@dapplets/unicode-escape-webpack-plugin": "^0.1.1" } } diff --git a/web-ssr/babel.config.json b/web-ssr/babel.config.json new file mode 100644 index 00000000..4f06b0cd --- /dev/null +++ b/web-ssr/babel.config.json @@ -0,0 +1,6 @@ +{ + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ] +} diff --git a/web-ssr/benchmark-node.mjs b/web-ssr/benchmark-node.mjs new file mode 100644 index 00000000..96b6d6b8 --- /dev/null +++ b/web-ssr/benchmark-node.mjs @@ -0,0 +1,14 @@ +// node standalone version of the benchmark for local testing. + +import { renderTest } from "./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.`); diff --git a/web-ssr/benchmark.js b/web-ssr/benchmark.js new file mode 100644 index 00000000..1a8f03d4 --- /dev/null +++ b/web-ssr/benchmark.js @@ -0,0 +1,95 @@ +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 { + // How many times (separate iterations) should we reuse the source code. + // Use 0 to skip. + CODE_REUSE_COUNT = 1; + iterationCount = 0; + iteration = 0; + lastResult = {}; + sourceCode; + sourceHash = 0 + iterationSourceCodes = []; + + constructor(iterationCount) { + this.iterationCount = iterationCount + } + + async init() { + this.sourceCode = await JetStream.getString(JetStream.preload.BUNDLE_BLOB); + this.expect("Cache Comment Count", this.sourceCode.match(CACHE_BUST_COMMENT_RE).length, 597); + for (let i = 0; i < this.iterationCount; i++) + this.iterationSourceCodes[i] = this.prepareCode(i); + } + + prepareCode(iteration) { + if (!this.CODE_REUSE_COUNT) + return this.sourceCode; + // Alter the code per iteration to prevent caching. + const cacheId = Math.floor(iteration / this.CODE_REUSE_COUNT); + const previousSourceCode = this.iterationSourceCodes[cacheId]; + if (previousSourceCode) + return previousSourceCode + const sourceCode = this.sourceCode.replaceAll(CACHE_BUST_COMMENT_RE, `/*${cacheId}*/`); + // Ensure efficient string representation. + this.sourceHash = quickHash(sourceCode); + 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/bundle.js b/web-ssr/dist/bundle.js new file mode 100644 index 00000000..06e995d1 --- /dev/null +++ b/web-ssr/dist/bundle.js @@ -0,0 +1,1221 @@ +/*! For license information please see bundle.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("