-
-
Notifications
You must be signed in to change notification settings - Fork 2
perf: Fix worker test execution excessive delay #519
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| /* | ||
| * @nevware21/ts-utils | ||
| * https://github.com/nevware21/ts-utils | ||
| * | ||
| * Copyright (c) 2026 NevWare21 Solutions LLC | ||
| * Licensed under the MIT license. | ||
| */ | ||
|
|
||
| /** | ||
| * Worker Test Adapter - Runs in main page context | ||
| * Creates Web Worker, loads tests, and reports results to Karma | ||
| */ | ||
|
|
||
| (function () { | ||
| "use strict"; | ||
|
|
||
| function initWorkerAdapter() { | ||
| var karma = window.__karma__; | ||
|
|
||
| if (!karma) { | ||
| console.log("[worker-adapter] __karma__ not available yet, will retry"); | ||
| setTimeout(initWorkerAdapter, 50); | ||
| return; | ||
| } | ||
|
|
||
| console.log("[worker-adapter] Starting worker test setup"); | ||
|
|
||
| if (window.mocha && typeof window.mocha.run === "function") { | ||
| window.mocha.run = function () { | ||
| console.log("[worker-adapter] Mocha.run intercepted - running tests in worker"); | ||
| startWorkerTests(); | ||
| return { | ||
| on: function () { return this; } | ||
| }; | ||
| }; | ||
| } else { | ||
| console.log("[worker-adapter] Mocha not found"); | ||
| } | ||
|
|
||
| function startWorkerTests() { | ||
| console.log("[worker-adapter] Initializing worker tests"); | ||
|
|
||
| var bundleFiles = []; | ||
| var moduleShims = []; | ||
| var jsFiles = []; | ||
| var files = karma.files; | ||
| var mochaFile = null; | ||
| var commonjsFile = null; | ||
|
|
||
| console.log("[worker-adapter] Total files from karma: " + Object.keys(files).length); | ||
|
|
||
| for (var file in files) { | ||
| if (!files.hasOwnProperty(file)) { | ||
| continue; | ||
| } | ||
|
|
||
| if (/karma-typescript-bundle-.*\.js(\?|$)/.test(file)) { | ||
| bundleFiles.push(file); | ||
| continue; | ||
| } | ||
|
|
||
| if (!mochaFile && /\/node_modules\/mocha\/mocha\.js(\?|$)/.test(file)) { | ||
| mochaFile = file; | ||
| continue; | ||
| } | ||
|
|
||
| if (!commonjsFile && /\/node_modules\/karma-typescript\/dist\/client\/commonjs\.js(\?|$)/.test(file)) { | ||
| commonjsFile = file; | ||
| continue; | ||
| } | ||
|
|
||
| if (/\.js(\?|$)/.test(file) && file.indexOf("/base/") === 0) { | ||
| jsFiles.push(file); | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| var testFiles = []; | ||
| var sourceFiles = []; | ||
| var testSupportFiles = []; | ||
| for (var i = 0; i < jsFiles.length; i++) { | ||
| if (/\/lib\/test\/src\/.*\.test\.js(\?|$)/.test(jsFiles[i])) { | ||
| testFiles.push(jsFiles[i]); | ||
| } else if (/^\/base\/lib\/src\/.*\.js(\?|$)/.test(jsFiles[i])) { | ||
| sourceFiles.push(jsFiles[i]); | ||
| } else if (/^\/base\/lib\/test\/src\/.*\.js(\?|$)/.test(jsFiles[i])) { | ||
| testSupportFiles.push(jsFiles[i]); | ||
| } | ||
| } | ||
|
|
||
| console.log("[worker-adapter] Found " + bundleFiles.length + " bundle files, " + | ||
| sourceFiles.length + " source files, " + | ||
| testSupportFiles.length + " support files, " + | ||
| testFiles.length + " test files, and " + | ||
| moduleShims.length + " module shims"); | ||
|
|
||
| var workerScript = null; | ||
| for (var fileName in files) { | ||
| if (files.hasOwnProperty(fileName) && fileName.indexOf("worker-test-runner.js") !== -1) { | ||
| workerScript = fileName; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (!workerScript) { | ||
| console.error("[worker-adapter] Could not find worker-test-runner.js"); | ||
| karma.error("[worker-test] Worker runner not found"); | ||
| karma.complete({}); | ||
| return; | ||
| } | ||
|
|
||
| console.log("[worker-adapter] Found worker script at: " + workerScript); | ||
|
|
||
| try { | ||
| var worker = new Worker(workerScript); | ||
| console.log("[worker-adapter] Worker created"); | ||
|
|
||
| worker.onmessage = function (event) { | ||
| var msg = event.data; | ||
|
|
||
| switch (msg.type) { | ||
| case "log": | ||
| console.log("[worker] " + msg.message); | ||
| break; | ||
|
|
||
| case "ready": | ||
| console.log("[worker-adapter] Worker ready, sending files"); | ||
| var filesToSend = bundleFiles.concat(sourceFiles, testSupportFiles, testFiles); | ||
| console.log("[worker-adapter] Sending " + filesToSend.length + " total files to worker"); | ||
| worker.postMessage({ | ||
| type: "loadTests", | ||
| files: filesToSend, | ||
| entrypoints: [], | ||
| moduleShims: moduleShims, | ||
| basePath: (karma.config && karma.config.basePath) ? karma.config.basePath : "", | ||
| mochaUrl: mochaFile || "/base/node_modules/mocha/mocha.js", | ||
| commonjsUrl: commonjsFile || "/base/node_modules/karma-typescript/dist/client/commonjs.js" | ||
| }); | ||
| break; | ||
|
|
||
| case "result": | ||
| karma.result(msg.result); | ||
| break; | ||
|
|
||
| case "complete": | ||
| karma.complete(msg.coverage ? { coverage: msg.coverage } : {}); | ||
| break; | ||
|
|
||
| case "error": | ||
| console.error("[worker-adapter] Worker error: " + msg.error); | ||
| karma.error("[worker-test] " + msg.error); | ||
| karma.complete({}); | ||
| break; | ||
|
Comment on lines
+149
to
+153
|
||
| } | ||
| }; | ||
|
|
||
| worker.onerror = function (error) { | ||
| console.error("[worker-adapter] Worker error event: " + error.message); | ||
| karma.error("[worker-test] Worker error: " + error.message); | ||
| karma.complete({}); | ||
| }; | ||
| } catch (err) { | ||
| console.error("[worker-adapter] Failed to create worker: " + err.message); | ||
| karma.error("[worker-test] Failed to create worker: " + err.message); | ||
| karma.complete({}); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| initWorkerAdapter(); | ||
| })(); | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,218 @@ | ||||||||
| /* | ||||||||
| * @nevware21/ts-utils | ||||||||
| * https://github.com/nevware21/ts-utils | ||||||||
| * | ||||||||
| * Copyright (c) 2026 NevWare21 Solutions LLC | ||||||||
| * Licensed under the MIT license. | ||||||||
| */ | ||||||||
|
|
||||||||
| /** | ||||||||
| * Worker Test Runner - Runs in Web Worker context | ||||||||
| * Loads and executes test files in worker environment | ||||||||
| */ | ||||||||
|
|
||||||||
| (function () { | ||||||||
| "use strict"; | ||||||||
|
|
||||||||
| function sendMessage(msg) { | ||||||||
| self.postMessage(msg); | ||||||||
| } | ||||||||
|
|
||||||||
| function workerLog(message) { | ||||||||
| sendMessage({ | ||||||||
| type: "log", | ||||||||
| message: message | ||||||||
| }); | ||||||||
| } | ||||||||
|
|
||||||||
| self.onmessage = function (event) { | ||||||||
| var msg = event.data; | ||||||||
|
|
||||||||
| if (msg.type === "loadTests") { | ||||||||
| loadAndRunTests(msg); | ||||||||
| } | ||||||||
| }; | ||||||||
|
|
||||||||
| function loadAndRunTests(payload) { | ||||||||
| var files = payload.files || []; | ||||||||
| var requestedEntrypoints = payload.entrypoints || []; | ||||||||
| var moduleShims = payload.moduleShims || []; | ||||||||
| var basePath = payload.basePath || ""; | ||||||||
| var mochaUrl = payload.mochaUrl; | ||||||||
| var commonjsUrl = payload.commonjsUrl; | ||||||||
|
|
||||||||
| workerLog("Loading " + files.length + " files and running " + requestedEntrypoints.length + " entrypoints"); | ||||||||
|
|
||||||||
| if (!mochaUrl || !commonjsUrl) { | ||||||||
| sendMessage({ | ||||||||
| type: "error", | ||||||||
| error: "Missing mocha or commonjs runtime URL" | ||||||||
| }); | ||||||||
| return; | ||||||||
| } | ||||||||
|
|
||||||||
| try { | ||||||||
| // Polyfill process for karma-typescript commonjs runtime | ||||||||
| if (typeof self.process === "undefined") { | ||||||||
| self.process = { | ||||||||
| env: { NODE_ENV: "test" }, | ||||||||
| cwd: function () { return "/"; }, | ||||||||
| browser: true | ||||||||
| }; | ||||||||
| } | ||||||||
|
|
||||||||
| importScripts(mochaUrl); | ||||||||
|
||||||||
| if (!self.mocha || !self.mocha.setup) { | ||||||||
| throw new Error("Mocha did not load in worker"); | ||||||||
| } | ||||||||
| self.mocha.setup({ | ||||||||
| ui: "bdd", | ||||||||
| reporter: function () {} | ||||||||
| }); | ||||||||
|
|
||||||||
| self.wrappers = self.wrappers || {}; | ||||||||
|
|
||||||||
| moduleShims.forEach(function (shim) { | ||||||||
| importScripts(shim.file); | ||||||||
|
||||||||
| registerShimWrapper(shim.name, shim.file, basePath); | ||||||||
| }); | ||||||||
|
|
||||||||
| for (var i = 0; i < files.length; i++) { | ||||||||
| importScripts(files[i]); | ||||||||
|
||||||||
| if (i % 50 === 0 || i === files.length - 1) { | ||||||||
| workerLog("Loaded " + (i + 1) + "/" + files.length + " files"); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| var wrapperKeys = Object.keys(self.wrappers || {}); | ||||||||
| workerLog("Available wrappers: " + wrapperKeys.length); | ||||||||
| if (wrapperKeys.length > 0) { | ||||||||
| workerLog("Sample wrappers: " + wrapperKeys.slice(0, 3).join(", ")); | ||||||||
| } | ||||||||
|
|
||||||||
| var resolvedEntrypoints = wrapperKeys.filter(function (key) { | ||||||||
| return /\/lib\/test\/src\/.*\.test\.(ts|js)$/.test(key); | ||||||||
| }); | ||||||||
| workerLog("Found " + resolvedEntrypoints.length + " test wrappers"); | ||||||||
| if (resolvedEntrypoints.length === 0 && requestedEntrypoints.length > 0) { | ||||||||
| workerLog("No wrapper entrypoints found; falling back to requested entrypoints"); | ||||||||
| self.entrypointFilenames = requestedEntrypoints.slice(); | ||||||||
| } else { | ||||||||
| self.entrypointFilenames = resolvedEntrypoints; | ||||||||
| } | ||||||||
|
|
||||||||
| importScripts(commonjsUrl); | ||||||||
|
||||||||
|
|
||||||||
| var runner = self.mocha.run(); | ||||||||
|
|
||||||||
| function getSuiteTitles(test) { | ||||||||
| var titles = []; | ||||||||
| var parent = test && test.parent; | ||||||||
|
|
||||||||
| while (parent) { | ||||||||
| if (parent.title) { | ||||||||
| titles.unshift(parent.title); | ||||||||
| } | ||||||||
| parent = parent.parent; | ||||||||
| } | ||||||||
|
|
||||||||
| return titles; | ||||||||
| } | ||||||||
|
|
||||||||
| runner.on("pass", function (test) { | ||||||||
| var suiteTitles = getSuiteTitles(test); | ||||||||
| sendMessage({ | ||||||||
| type: "result", | ||||||||
| result: { | ||||||||
| description: test.title, | ||||||||
| suite: suiteTitles, | ||||||||
| success: true, | ||||||||
| skipped: false, | ||||||||
| time: test.duration, | ||||||||
| log: [] | ||||||||
| } | ||||||||
| }); | ||||||||
| }); | ||||||||
|
|
||||||||
| runner.on("pending", function (test) { | ||||||||
| var suiteTitles = getSuiteTitles(test); | ||||||||
| sendMessage({ | ||||||||
| type: "result", | ||||||||
| result: { | ||||||||
| description: test.title, | ||||||||
| suite: suiteTitles, | ||||||||
| success: true, | ||||||||
| skipped: true, | ||||||||
| log: [] | ||||||||
| } | ||||||||
| }); | ||||||||
| }); | ||||||||
|
|
||||||||
| runner.on("fail", function (test, err) { | ||||||||
| var suiteTitles = getSuiteTitles(test); | ||||||||
| sendMessage({ | ||||||||
| type: "result", | ||||||||
| result: { | ||||||||
| description: test.title, | ||||||||
| suite: suiteTitles, | ||||||||
| success: false, | ||||||||
| skipped: false, | ||||||||
| log: [err && err.message ? err.message : "Test failed"], | ||||||||
| time: test.duration | ||||||||
| } | ||||||||
| }); | ||||||||
| }); | ||||||||
|
|
||||||||
| runner.on("end", function () { | ||||||||
| sendMessage({ | ||||||||
| type: "complete", | ||||||||
| coverage: self.__coverage__ | ||||||||
| }); | ||||||||
|
||||||||
| }); | |
| }); | |
| self.close(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After receiving the worker "complete" message, the Web Worker is left running. In watch/debug sessions this can leak workers and keep resources alive. Consider terminating the worker after calling karma.complete (and similarly on error paths).