// Plain CommonJS worker_threads entry point, NOT part of the Meteor imports // graph (lives under private/ so `meteor build` copies it verbatim into // programs/server/assets/app/workers/unionWorker.js instead of compiling it). // // Runs the CPU-heavy part of the subscriptions geo-union (circle generation + // sequential turf.union chain) off the main thread. A single tunion() call // against an already-complex merged polygon can itself take seconds once // there are thousands of subscriptions — yielding between iterations on the // main thread doesn't help when one iteration alone blocks that long, so this // needs a real worker thread, not just cooperative scheduling. // // workerData.subs must already be validated (location.lat/lon/distance // present) and decorated (addNoisy/noNoisy already applied) by the caller. // workerData.baseUnion, if given, is an already-computed union GeoJSON that // subs gets merged into (the incremental-add fast path) instead of starting // from scratch — for a single new subscription this is one tunion() call // instead of redoing the whole chain over every subscription again. // workerData.turfPaths gives absolute paths to @turf/circle, @turf/union and // @turf/truncate — this file lives under programs/server/assets/, while the // app's own npm deps live in the sibling programs/server/npm/node_modules, // which plain require('@turf/circle') can never find by walking up parent // directories. The caller resolves the paths (it's Meteor-compiled code that // knows how to find them) and hands them over instead. const { parentPort, workerData } = require('worker_threads'); // eslint-disable-next-line import/no-dynamic-require const tcircle = require(workerData.turfPaths.circle).default; // eslint-disable-next-line import/no-dynamic-require const tunion = require(workerData.turfPaths.union).default; // eslint-disable-next-line import/no-dynamic-require const ttrunc = require(workerData.turfPaths.truncate).default; const truncOptions = { precision: 6, coordinates: 2 }; // Vértices por círculo: la palanca más directa sobre lo que tarda la unión y // sobre la memoria que se come. Medido con 10.000 suscripciones // (bench/union-bench.js): 144 → 23,5 s y 777 MB de pico; 64 → 10,0 s y 421 MB; // 32 → 4,7 s y 267 MB. Con 64 un círculo sigue siendo un círculo en pantalla // (la capa de zonas es un adorno difuso, no una geometría que nadie mida) y el // host de despliegue solo tiene 5,9 GB para todo el stack, así que 64 es el // valor por defecto. Se puede volver a 144 con // `Meteor.settings.private.unionCircleSteps`. const DEFAULT_STEPS = 64; // Unión en ÁRBOL, no en cadena. // // Antes esto era `u = union(u, circulo[i])` en bucle: cada llamada trabajaba // contra un polígono acumulado que no paraba de crecer, así que el coste subía // mucho más deprisa que el número de suscripciones. Uniendo por pares (y luego // pares de pares) la mayoría de las uniones son entre polígonos pequeños y solo // las últimas son caras. El resultado geométrico es el mismo — la unión es // asociativa y conmutativa — y así lo comprueba test/server/calcUnionAsync.test.js // contra una referencia calculada en cadena. // // Medido en bench/union-bench.js con 1.000 suscripciones repartidas por el mundo // (el peor caso, círculos que casi no se solapan): 238 s en cadena frente a // 3,6 s en árbol, con un documento idéntico. Con datos realistas (España) la // mejora es menor porque los círculos se funden pronto, pero va en la misma // dirección. const run = () => { const { subs, baseUnion, steps } = workerData; if (subs.length === 0) return baseUnion || null; const circleAt = (i) => tcircle( [subs[i].location.lon, subs[i].location.lat], subs[i].distance, { units: 'kilometers', steps: steps || DEFAULT_STEPS } ); // Primer nivel: los círculos se generan AL VUELO y se unen de dos en dos, en // vez de materializar los N y luego unirlos. Con 10.000 suscripciones eso es // la diferencia entre un pico de ~1 GB y uno de la mitad, y el host de // despliegue tiene 5,9 GB para todo el stack. let level = []; for (let i = 0; i < subs.length; i += 2) { if (i + 1 < subs.length) level.push(ttrunc(tunion(circleAt(i), circleAt(i + 1)), truncOptions)); else level.push(ttrunc(circleAt(i), truncOptions)); } while (level.length > 1) { const next = []; for (let i = 0; i < level.length; i += 2) { if (i + 1 < level.length) next.push(ttrunc(tunion(level[i], level[i + 1]), truncOptions)); else next.push(level[i]); } level = next; } return baseUnion ? ttrunc(tunion(baseUnion, level[0]), truncOptions) : level[0]; }; try { parentPort.postMessage({ union: run() }); } catch (e) { parentPort.postMessage({ error: e.message }); }