11import { deserialize , serialize } from "./shared.ts" ;
22import type { ThreadTask , WorkerResponsePayload } from "./types.ts" ;
33
4- /**
5- * In Vite, the worker detection will only work if the new URL() constructor is used directly inside the new Worker() declaration.
6- * Additionally, all options parameters must be static values (i.e. string literals).
7- */
84let newWorker : ( ) => Worker ;
95
106export function workerOverride ( fn : ( ) => Worker ) {
117 newWorker = fn ;
128}
139
10+ interface TrackedWorker extends Worker {
11+ // Set of function IDs this worker has already compiled
12+ _loadedFnIds : Set < string > ;
13+ _pending : Map <
14+ number ,
15+ { resolve : ( val : any ) => void ; reject : ( err : any ) => void }
16+ > ;
17+ }
18+
1419export class WorkerPool {
15- private workers : Worker [ ] = [ ] ;
16- private workerLoad = new Map < Worker , number > ( ) ;
17- private pending = new Map <
18- Worker ,
19- Map < number , { resolve : ( val : any ) => void ; reject : ( err : any ) => void } >
20- > ( ) ;
21-
22- private codeCache = new Map < string , Promise < string > > ( ) ;
20+ private workers : TrackedWorker [ ] = [ ] ;
2321 private maxThreads : number ;
2422 private taskIdCounter = 0 ;
2523
2624 constructor ( maxThreads ?: number ) {
27- this . maxThreads = maxThreads ?? navigator . hardwareConcurrency ?? 4 ;
25+ this . maxThreads = maxThreads || navigator . hardwareConcurrency || 4 ;
2826 }
2927
30- private spawnWorker ( ) : Worker {
31- const worker = newWorker ( ) ;
32- this . pending . set ( worker , new Map ( ) ) ;
33- this . workerLoad . set ( worker , 0 ) ;
28+ private createWorker ( ) : TrackedWorker {
29+ const worker = newWorker ( ) as TrackedWorker ;
30+
31+ worker . _loadedFnIds = new Set ( ) ;
32+ worker . _pending = new Map ( ) ;
3433
35- // 1. Success / Task Error Handler
3634 worker . onmessage = ( e : MessageEvent < WorkerResponsePayload > ) => {
3735 const { taskId, type } = e . data ;
38- const workerPending = this . pending . get ( worker ) ;
39- const p = workerPending ?. get ( taskId ) ;
36+ const p = worker . _pending . get ( taskId ) ;
4037
4138 if ( p ) {
42- this . workerLoad . set (
43- worker ,
44- Math . max ( 0 , this . workerLoad . get ( worker ) ?? 0 - 1 ) ,
45- ) ;
46-
4739 if ( type === "ERROR" ) {
4840 const err = new Error ( e . data . error ) ;
4941 if ( e . data . stack ) err . stack = e . data . stack ;
5042 p . reject ( err ) ;
5143 } else {
52- // Rehydrate the result (Restore Mutex/Channel methods)
53- const result = deserialize ( e . data . result ) ;
54- p . resolve ( result ) ;
44+ p . resolve ( deserialize ( e . data . result ) ) ;
5545 }
56- workerPending ? .delete ( taskId ) ;
46+ worker . _pending . delete ( taskId ) ;
5747 }
5848 } ;
5949
60- // 2. Crash Handler
6150 worker . onerror = ( e ) => {
6251 e . preventDefault ( ) ;
63- const workerPending = this . pending . get ( worker ) ;
64- if ( workerPending ) {
65- for ( const [ _ , p ] of workerPending ) {
66- p . reject ( new Error ( `Worker Crashed: ${ e . message } ` ) ) ;
67- }
68- workerPending . clear ( ) ;
69- }
70- this . workerLoad . delete ( worker ) ;
71- this . pending . delete ( worker ) ;
72- this . workers = this . workers . filter ( ( w ) => w !== worker ) ;
52+ const err = new Error ( `Worker Crashed: ${ e . message } ` ) ;
53+ for ( const p of worker . _pending . values ( ) ) p . reject ( err ) ;
54+ worker . _pending . clear ( ) ;
55+ this . removeWorker ( worker ) ;
7356 } ;
7457
7558 this . workers . push ( worker ) ;
7659 return worker ;
7760 }
7861
79- async submit ( task : ThreadTask ) : Promise < any > {
80- const { fnId, code, args } = task ;
62+ private removeWorker ( worker : TrackedWorker ) {
63+ this . workers = this . workers . filter ( ( w ) => w !== worker ) ;
64+ worker . terminate ( ) ;
65+ }
8166
82- // Select Worker (Least Loaded)
83- let selectedWorker : Worker ;
84- if ( this . workers . length < this . maxThreads ) {
85- selectedWorker = this . spawnWorker ( ) ;
86- } else {
87- selectedWorker = this . workers . reduce ( ( prev , curr ) => {
88- const prevLoad = this . workerLoad . get ( prev ) ! ;
89- const currLoad = this . workerLoad . get ( curr ) ! ;
90- return prevLoad < currLoad ? prev : curr ;
91- } ) ;
92- }
67+ private async executeTask (
68+ worker : TrackedWorker ,
69+ task : ThreadTask ,
70+ ) : Promise < any > {
71+ const { fnId, code, args } = task ;
72+ const taskId = this . taskIdCounter ++ ;
9373
9474 const { promise, resolve, reject } = Promise . withResolvers ( ) ;
9575
96- const taskId = this . taskIdCounter ++ ;
97- this . pending . get ( selectedWorker ) ! . set ( taskId , { resolve, reject } ) ;
98- this . workerLoad . set (
99- selectedWorker ,
100- ( this . workerLoad . get ( selectedWorker ) || 0 ) + 1 ,
101- ) ;
76+ worker . _pending . set ( taskId , { resolve, reject } ) ;
10277
103- // Serialize Args & Unify Transferables
10478 const serializedArgs = args . map ( serialize ) ;
10579 const values = serializedArgs . map ( ( r ) => r . value ) ;
10680 const transferList = [
10781 ...new Set ( serializedArgs . flatMap ( ( r ) => r . transfer ) ) ,
10882 ] ;
10983
110- selectedWorker . postMessage (
84+ const hasCode = worker . _loadedFnIds . has ( fnId ) ;
85+ if ( ! hasCode ) {
86+ worker . _loadedFnIds . add ( fnId ) ;
87+ }
88+
89+ worker . postMessage (
11190 {
11291 type : "RUN" ,
11392 taskId,
11493 fnId,
115- code,
94+ code : hasCode ? undefined : code ,
11695 args : values ,
11796 } ,
11897 { transfer : transferList } ,
@@ -121,11 +100,49 @@ export class WorkerPool {
121100 return await promise ;
122101 }
123102
103+ async submit ( task : ThreadTask ) : Promise < any > {
104+ let bestCandidate : TrackedWorker | undefined ;
105+ let bestCandidateLoad = Infinity ;
106+ // Score: 0 = Idle+Affinity, 1 = Idle, 2 = Busy+Affinity, 3 = Busy
107+ let bestCandidateScore = 4 ;
108+
109+ for ( let i = 0 ; i < this . workers . length ; i ++ ) {
110+ const w = this . workers [ i ] ! ;
111+ const load = w . _pending . size ;
112+ const hasAffinity = w . _loadedFnIds . has ( task . fnId ) ;
113+
114+ if ( load === 0 && hasAffinity ) {
115+ return await this . executeTask ( w , task ) ;
116+ }
117+
118+ let score = 4 ;
119+ if ( load === 0 ) score = 1 ;
120+ else if ( hasAffinity ) score = 2 ;
121+ else score = 3 ;
122+
123+ if (
124+ score < bestCandidateScore ||
125+ ( score === bestCandidateScore && load < bestCandidateLoad )
126+ ) {
127+ bestCandidate = w ;
128+ bestCandidateScore = score ;
129+ bestCandidateLoad = load ;
130+ }
131+ }
132+
133+ if ( bestCandidateScore >= 2 && this . workers . length < this . maxThreads ) {
134+ return await this . executeTask ( this . createWorker ( ) , task ) ;
135+ }
136+
137+ if ( bestCandidate ) {
138+ return await this . executeTask ( bestCandidate , task ) ;
139+ }
140+
141+ return await this . executeTask ( this . createWorker ( ) , task ) ;
142+ }
143+
124144 terminate ( ) {
125145 for ( const w of this . workers ) w . terminate ( ) ;
126146 this . workers = [ ] ;
127- this . workerLoad . clear ( ) ;
128- this . pending . clear ( ) ;
129- this . codeCache . clear ( ) ;
130147 }
131148}
0 commit comments