'use strict';
const { workerData, parentPort } = require('worker_threads');
const { performance } = require('perf_hooks');
let pm;
try {
pm = require('../pkg-nodejs/wasm4pm.js');
} catch (_) {
try {
pm = require('../pkg/wasm4pm.js');
} catch (e) {
parentPort.postMessage({ error: `Cannot load WASM module: ${e.message}` });
process.exit(1);
}
}
pm.init();
const ACTIVITY_KEY = 'concept:name';
const TIMESTAMP_KEY = 'time:timestamp';
function lcg(seed) {
let s = BigInt(seed);
return {
next() {
s = (s * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn;
return Number(s & 0x7fffffffffffffffn);
},
nextFloat() {
return (this.next() >>> 11) / 2 ** 53;
},
nextMod(m) {
return this.next() % m;
},
};
}
const ACTIVITIES = [
'Register',
'Validate',
'Check_Completeness',
'Check_Docs',
'Assess_Risk',
'Calculate_Fee',
'Send_Invoice',
'Wait_Payment',
'Confirm_Payment',
'Approve_Basic',
'Approve_Senior',
'Approve_Director',
'Notify_Applicant',
'Create_Record',
'Archive',
'Close',
'Reject',
'Escalate',
'Return_Docs',
'Reopen',
];
function generateXES(numCases, numActivities = 12, avgEvents = 15, noiseFactor = 0.1) {
const acts = ACTIVITIES.slice(0, numActivities);
const rng = lcg(0xdeadbeefcafebabe);
const lines = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<log xes.version="1.0" xes.features="nested-attributes">',
];
for (let caseIdx = 0; caseIdx < numCases; caseIdx++) {
lines.push(' <trace>');
lines.push(` <string key="concept:name" value="case_${caseIdx}"/>`);
const lenFactor = 0.5 + rng.nextFloat();
const numEvents = Math.max(2, Math.floor(avgEvents * lenFactor));
for (let evtIdx = 0; evtIdx < numEvents; evtIdx++) {
const baseIdx = evtIdx % acts.length;
const actIdx = rng.nextFloat() < noiseFactor ? rng.nextMod(acts.length) : baseIdx;
const hour = Math.floor(evtIdx / 60) % 24;
const min = evtIdx % 60;
const day = (caseIdx % 28) + 1;
lines.push(' <event>');
lines.push(` <string key="concept:name" value="${acts[actIdx]}"/>`);
lines.push(
` <date key="time:timestamp" value="2024-01-${String(day).padStart(2, '0')}T${String(hour).padStart(2, '0')}:${String(min).padStart(2, '0')}:00.000+00:00"/>`
);
lines.push(' </event>');
}
lines.push(' </trace>');
}
lines.push('</log>');
return lines.join('\n');
}
function callAlgorithm(name, handle, params) {
switch (name) {
case 'discover_dfg':
return pm.discover_dfg(handle, params.activityKey || ACTIVITY_KEY);
case 'discover_declare':
return pm.discover_declare(handle, params.activityKey || ACTIVITY_KEY);
case 'discover_heuristic_miner':
return pm.discover_heuristic_miner(
handle,
params.activityKey || ACTIVITY_KEY,
params.threshold ?? 0.5
);
case 'discover_alpha_plus_plus':
return pm.discover_alpha_plus_plus(
handle,
params.activityKey || ACTIVITY_KEY,
params.minSupport ?? 0.1
);
case 'discover_inductive_miner':
return pm.discover_inductive_miner(handle, params.activityKey || ACTIVITY_KEY);
case 'discover_hill_climbing':
return pm.discover_hill_climbing(handle, params.activityKey || ACTIVITY_KEY);
case 'extract_process_skeleton':
return pm.extract_process_skeleton(
handle,
params.activityKey || ACTIVITY_KEY,
params.minFreq ?? 2
);
case 'discover_astar':
return pm.discover_astar(handle, params.activityKey || ACTIVITY_KEY, params.maxIter ?? 500);
case 'discover_simulated_annealing':
return pm.discover_simulated_annealing(
handle,
params.activityKey || ACTIVITY_KEY,
params.temp ?? 100.0,
params.cooling ?? 0.95
);
case 'discover_ant_colony':
return pm.discover_ant_colony(
handle,
params.activityKey || ACTIVITY_KEY,
params.ants ?? 20,
params.iterations ?? 10
);
case 'discover_genetic_algorithm':
return pm.discover_genetic_algorithm(
handle,
params.activityKey || ACTIVITY_KEY,
params.popSize ?? 10,
params.generations ?? 5
);
case 'discover_pso_algorithm':
return pm.discover_pso_algorithm(
handle,
params.activityKey || ACTIVITY_KEY,
params.swarm ?? 10,
params.iterations ?? 10
);
case 'discover_ilp_petri_net':
return pm.discover_ilp_petri_net(handle, params.activityKey || ACTIVITY_KEY);
case 'analyze_trace_variants':
return pm.analyze_trace_variants(handle, params.activityKey || ACTIVITY_KEY);
case 'mine_sequential_patterns':
return pm.mine_sequential_patterns(
handle,
params.activityKey || ACTIVITY_KEY,
params.minSupport ?? 0.1,
params.patternLen ?? 3
);
case 'detect_concept_drift':
return pm.detect_concept_drift(
handle,
params.activityKey || ACTIVITY_KEY,
params.windowSize ?? 50
);
case 'cluster_traces':
return pm.cluster_traces(handle, params.activityKey || ACTIVITY_KEY, params.numClusters ?? 5);
case 'analyze_variant_complexity':
return pm.analyze_variant_complexity(handle, params.activityKey || ACTIVITY_KEY);
case 'compute_activity_transition_matrix':
return pm.compute_activity_transition_matrix(handle, params.activityKey || ACTIVITY_KEY);
case 'detect_rework':
return pm.detect_rework(handle, params.activityKey || ACTIVITY_KEY);
case 'analyze_event_statistics':
return pm.analyze_event_statistics(handle);
default:
throw new Error(`Unknown algorithm: ${name}`);
}
}
function median(arr) {
const s = [...arr].sort((a, b) => a - b);
const mid = Math.floor(s.length / 2);
return s.length % 2 === 0 ? (s[mid - 1] + s[mid]) / 2 : s[mid];
}
function percentile(arr, p) {
const s = [...arr].sort((a, b) => a - b);
return s[Math.floor((s.length * p) / 100)];
}
const { tasks, ciMode } = workerData;
const ITERATIONS = ciMode ? 3 : 7;
const results = [];
for (const task of tasks) {
for (const size of task.sizes) {
const xes = generateXES(size, task.numActivities || 12);
const handle = pm.load_eventlog_from_xes(xes);
try {
callAlgorithm(task.algorithm, handle, task.params || {});
} catch (_) {}
const timings = [];
for (let i = 0; i < ITERATIONS; i++) {
const t0 = performance.now();
try {
callAlgorithm(task.algorithm, handle, task.params || {});
} catch (_) {}
timings.push(performance.now() - t0);
}
results.push({
algorithm: task.algorithm,
size,
medianMs: median(timings),
minMs: Math.min(...timings),
maxMs: Math.max(...timings),
p95Ms: percentile(timings, 95),
iterations: ITERATIONS,
});
pm.clear_all_objects();
}
}
parentPort.postMessage(results);