bulltrackers-module 1.0.278 → 1.0.280

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  * UPDATED: Implements Batch Flushing to prevent OOM on large datasets.
4
4
  * UPDATED: Removes manual global.gc() calls.
5
5
  * UPDATED: Manages incremental sharding states.
6
+ * UPDATED (IDEA 2): Implemented Computation Profiler (timings).
6
7
  */
7
8
  const { normalizeName } = require('../utils/utils');
8
9
  const { streamPortfolioData, streamHistoryData, getPortfolioPartRefs } = require('../utils/data_loader');
@@ -10,6 +11,7 @@ const { CachedDataLoader } = require
10
11
  const { ContextFactory } = require('../context/ContextFactory');
11
12
  const { commitResults } = require('../persistence/ResultCommitter');
12
13
  const mathLayer = require('../layers/index');
14
+ const { performance } = require('perf_hooks');
13
15
 
14
16
  class StandardExecutor {
15
17
  static async run(date, calcs, passName, config, deps, rootData, fetchedDeps, previousFetchedDeps, skipStatusWrite = false) {
@@ -53,20 +55,31 @@ class StandardExecutor {
53
55
 
54
56
  logger.log('INFO', `[${passName}] Streaming for ${streamingCalcs.length} computations...`);
55
57
 
56
- // Metrics & State Tracking
58
+ // [IDEA 2] Metrics & State Tracking
57
59
  const executionStats = {};
58
60
  const shardIndexMap = {}; // Tracks sharding offsets per calculation
59
61
  const aggregatedSuccess = {};
60
62
  const aggregatedFailures = [];
61
63
 
64
+ // Initialize Timing Stats per calculation
62
65
  Object.keys(state).forEach(name => {
63
- executionStats[name] = { processedUsers: 0, skippedUsers: 0 };
66
+ executionStats[name] = {
67
+ processedUsers: 0,
68
+ skippedUsers: 0,
69
+ timings: { setup: 0, stream: 0, processing: 0 } // New
70
+ };
64
71
  shardIndexMap[name] = 0;
65
72
  });
66
73
 
74
+ // [IDEA 2] Measure Setup Time
75
+ const startSetup = performance.now();
67
76
  const cachedLoader = new CachedDataLoader(config, deps);
68
77
  await cachedLoader.loadMappings();
78
+ const setupDuration = performance.now() - startSetup;
69
79
 
80
+ // Distribute setup time
81
+ Object.keys(executionStats).forEach(name => executionStats[name].timings.setup += setupDuration);
82
+
70
83
  const prevDate = new Date(dateStr + 'T00:00:00Z'); prevDate.setUTCDate(prevDate.getUTCDate() - 1);
71
84
  const prevDateStr = prevDate.toISOString().slice(0, 10);
72
85
 
@@ -83,13 +96,19 @@ class StandardExecutor {
83
96
  let usersSinceLastFlush = 0;
84
97
 
85
98
  try {
99
+ // [IDEA 2] Loop wrapper for profiling
86
100
  for await (const tP_chunk of tP_iter) {
101
+ // [IDEA 2] Measure Streaming Time (Gap between processing chunks)
102
+ const startStream = performance.now();
87
103
  if (yP_iter) yP_chunk = (await yP_iter.next()).value || {};
88
104
  if (tH_iter) tH_chunk = (await tH_iter.next()).value || {};
89
-
105
+ const streamDuration = performance.now() - startStream;
106
+ Object.keys(executionStats).forEach(name => executionStats[name].timings.stream += streamDuration);
107
+
90
108
  const chunkSize = Object.keys(tP_chunk).length;
91
109
 
92
- // Execute chunk for all calcs
110
+ // [IDEA 2] Measure Processing Time
111
+ const startProcessing = performance.now();
93
112
  const promises = streamingCalcs.map(calc =>
94
113
  StandardExecutor.executePerUser(
95
114
  calc, calc.manifest, dateStr, tP_chunk, yP_chunk, tH_chunk,
@@ -98,6 +117,10 @@ class StandardExecutor {
98
117
  )
99
118
  );
100
119
  await Promise.all(promises);
120
+ const procDuration = performance.now() - startProcessing;
121
+
122
+ // Assign processing time (Note: Parallel execution means total wall time is shared)
123
+ Object.keys(executionStats).forEach(name => executionStats[name].timings.processing += procDuration);
101
124
 
102
125
  usersSinceLastFlush += chunkSize;
103
126
 
@@ -161,7 +184,7 @@ class StandardExecutor {
161
184
  transformedState[name] = {
162
185
  manifest: inst.manifest,
163
186
  getResult: async () => dataToCommit,
164
- _executionStats: executionStats[name] // Attach current stats
187
+ _executionStats: executionStats[name] // Attach current stats including timings
165
188
  };
166
189
 
167
190
  // ⚠️ CRITICAL: CLEAR MEMORY
@@ -196,6 +219,18 @@ class StandardExecutor {
196
219
  successAcc[name].metrics.storage.keys += (update.metrics.storage.keys || 0);
197
220
  successAcc[name].metrics.storage.shardCount = Math.max(successAcc[name].metrics.storage.shardCount, update.metrics.storage.shardCount || 1);
198
221
  }
222
+
223
+ // [IDEA 2] Sum timing metrics
224
+ if (update.metrics?.execution?.timings) {
225
+ if (!successAcc[name].metrics.execution) successAcc[name].metrics.execution = { timings: { setup:0, stream:0, processing:0 }};
226
+ const tDest = successAcc[name].metrics.execution.timings;
227
+ const tSrc = update.metrics.execution.timings;
228
+
229
+ tDest.setup += (tSrc.setup || 0);
230
+ tDest.stream += (tSrc.stream || 0);
231
+ tDest.processing += (tSrc.processing || 0);
232
+ }
233
+
199
234
  // Keep the latest hash/composition info
200
235
  successAcc[name].hash = update.hash;
201
236
  }
@@ -11,7 +11,7 @@ const { generateProcessId, PROCESS_TYPES } = require('../logger/logger');
11
11
  const { HeuristicValidator } = require('./ResultsValidator');
12
12
  const validationOverrides = require('../config/validation_overrides');
13
13
  const pLimit = require('p-limit');
14
- const zlib = require('zlib'); // [NEW] Compression Lib
14
+ const zlib = require('zlib');
15
15
 
16
16
  const NON_RETRYABLE_ERRORS = [
17
17
  'PERMISSION_DENIED', 'DATA_LOSS', 'FAILED_PRECONDITION'
@@ -59,10 +59,11 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
59
59
  let effectiveOverrides = { ...configOverrides };
60
60
 
61
61
  if (isPriceOnly && !effectiveOverrides.weekend) {
62
+ // Apply strict leniency for weekend/holiday price actions
62
63
  effectiveOverrides.weekend = {
63
64
  maxZeroPct: 100,
64
65
  maxFlatlinePct: 100,
65
- maxNullPct: 100 // Allow full nulls (e.g. holidays)
66
+ maxNullPct: 100
66
67
  };
67
68
  }
68
69
  // -----------------------------------------------------------
@@ -193,17 +194,14 @@ async function commitResults(stateObj, dStr, passName, config, deps, skipStatusW
193
194
 
194
195
  async function writeSingleResult(result, docRef, name, dateContext, logger, config, deps, startShardIndex = 0, flushMode = 'STANDARD') {
195
196
 
196
- // --- [NEW] COMPRESSION STRATEGY ---
197
- // Try to compress before falling back to complex sharding
197
+ // --- COMPRESSION STRATEGY ---
198
198
  try {
199
199
  const jsonString = JSON.stringify(result);
200
200
  const rawBuffer = Buffer.from(jsonString);
201
201
 
202
- // Only attempt if meaningful size (> 50KB)
203
202
  if (rawBuffer.length > 50 * 1024) {
204
203
  const compressedBuffer = zlib.gzipSync(rawBuffer);
205
204
 
206
- // If compressed fits in one document (< 900KB safety limit)
207
205
  if (compressedBuffer.length < 900 * 1024) {
208
206
  logger.log('INFO', `[Compression] ${name}: Compressed ${(rawBuffer.length/1024).toFixed(0)}KB -> ${(compressedBuffer.length/1024).toFixed(0)}KB. Saved as Blob.`);
209
207
 
@@ -214,7 +212,6 @@ async function writeSingleResult(result, docRef, name, dateContext, logger, conf
214
212
  payload: compressedBuffer
215
213
  };
216
214
 
217
- // Write immediately
218
215
  await docRef.set(compressedPayload, { merge: true });
219
216
 
220
217
  return {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Utility for recording computation run attempts (The Audit Logger).
3
3
  * UPDATED: Stores 'trigger' reason and 'execution' stats.
4
+ * UPDATED (IDEA 2): Stores granular timing profiles.
4
5
  */
5
6
 
6
7
  const { FieldValue } = require('../utils/utils');
@@ -37,6 +38,10 @@ async function recordRunAttempt(db, context, status, error = null, detailedMetri
37
38
  const anomalies = detailedMetrics.validation?.anomalies || [];
38
39
  if (error && error.message && error.message.includes('Data Integrity')) { anomalies.push(error.message); }
39
40
 
41
+ // [IDEA 2] Prepare Execution Stats & Timings
42
+ const rawExecStats = detailedMetrics.execution || {};
43
+ const timings = rawExecStats.timings || {};
44
+
40
45
  const runEntry = {
41
46
  runId: runId,
42
47
  computationName: computation,
@@ -53,8 +58,17 @@ async function recordRunAttempt(db, context, status, error = null, detailedMetri
53
58
  type: (triggerReason && triggerReason.includes('Layer')) ? 'CASCADE' : ((triggerReason && triggerReason.includes('New')) ? 'INIT' : 'UPDATE')
54
59
  },
55
60
 
56
- // [NEW] Execution Stats (Internal Loop Data)
57
- executionStats: detailedMetrics.execution || {},
61
+ // [IDEA 2] Enhanced Execution Stats
62
+ executionStats: {
63
+ processedUsers: rawExecStats.processedUsers || 0,
64
+ skippedUsers: rawExecStats.skippedUsers || 0,
65
+ // Explicitly break out timings for BigQuery/Analysis
66
+ timings: {
67
+ setupMs: Math.round(timings.setup || 0),
68
+ streamMs: Math.round(timings.stream || 0),
69
+ processingMs: Math.round(timings.processing || 0)
70
+ }
71
+ },
58
72
 
59
73
  outputStats: {
60
74
  sizeMB: sizeMB,
@@ -64,7 +78,7 @@ async function recordRunAttempt(db, context, status, error = null, detailedMetri
64
78
  },
65
79
 
66
80
  anomalies: anomalies,
67
- _schemaVersion: '2.1'
81
+ _schemaVersion: '2.2' // Bumped for profiler
68
82
  };
69
83
 
70
84
  if (error) {
@@ -2,16 +2,15 @@
2
2
  * @fileoverview Build Reporter & Auto-Runner.
3
3
  * Generates a "Pre-Flight" report of what the computation system WILL do.
4
4
  * REFACTORED: Strict 5-category reporting with date-based exclusion logic.
5
- * UPDATED: Added transactional locking to prevent duplicate reports on concurrent cold starts.
6
- * UPDATED: Adds 'pass' number to detail records for better waterfall visibility.
5
+ * UPDATED: Replaced Batch Writes with Parallel Writes to prevent DEADLINE_EXCEEDED timeouts.
7
6
  * FIXED: Ensures 'latest' pointer updates even if detail writes fail.
7
+ * UPDATED (IDEA 1): Added Dependency Impact Analysis ("Blast Radius").
8
8
  */
9
9
 
10
10
  const { analyzeDateExecution } = require('../WorkflowOrchestrator');
11
11
  const { fetchComputationStatus } = require('../persistence/StatusRepository');
12
12
  const { normalizeName, getExpectedDateStrings, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils');
13
13
  const { checkRootDataAvailability } = require('../data/AvailabilityChecker');
14
- const { commitBatchInChunks } = require('../persistence/FirestoreUtils');
15
14
  const pLimit = require('p-limit');
16
15
  const path = require('path');
17
16
  const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
@@ -43,10 +42,37 @@ function isDateBeforeAvailability(dateStr, calcManifest) {
43
42
  return false;
44
43
  }
45
44
 
45
+ /**
46
+ * Helper: Calculates the transitive closure of dependents (Blast Radius).
47
+ * Returns the count of direct and total cascading dependents.
48
+ */
49
+ function calculateBlastRadius(targetCalcName, reverseGraph) {
50
+ const impactSet = new Set();
51
+ const queue = [targetCalcName];
52
+
53
+ // BFS Traversal
54
+ while(queue.length > 0) {
55
+ const current = queue.shift();
56
+ const dependents = reverseGraph.get(current) || [];
57
+
58
+ dependents.forEach(child => {
59
+ if (!impactSet.has(child)) {
60
+ impactSet.add(child);
61
+ queue.push(child);
62
+ }
63
+ });
64
+ }
65
+
66
+ return {
67
+ directDependents: (reverseGraph.get(targetCalcName) || []).length,
68
+ totalCascadingDependents: impactSet.size,
69
+ affectedCalculations: Array.from(impactSet).slice(0, 50) // Cap list size for storage safety
70
+ };
71
+ }
72
+
46
73
  /**
47
74
  * AUTO-RUN ENTRY POINT
48
- * UPDATED: Uses transactional locking to prevent race conditions.
49
- * If we deploy multiple computation pass nodes simultaneously, only one should run the report.
75
+ * Uses transactional locking to prevent race conditions.
50
76
  */
51
77
  async function ensureBuildReport(config, dependencies, manifest) {
52
78
  const { db, logger } = dependencies;
@@ -88,7 +114,7 @@ async function ensureBuildReport(config, dependencies, manifest) {
88
114
  }
89
115
 
90
116
  /**
91
- * Generates the report and saves to Firestore (Sharded).
117
+ * Generates the report and saves to Firestore.
92
118
  */
93
119
  async function generateBuildReport(config, dependencies, manifest, daysBack = 90, customBuildId = null) {
94
120
  const { db, logger } = dependencies;
@@ -103,6 +129,19 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
103
129
  const datesToCheck = getExpectedDateStrings(startDate, today);
104
130
  const manifestMap = new Map(manifest.map(c => [normalizeName(c.name), c]));
105
131
 
132
+ // [IDEA 1] Build Reverse Dependency Graph (Parent -> Children)
133
+ const reverseGraph = new Map();
134
+ manifest.forEach(c => {
135
+ const parentName = normalizeName(c.name);
136
+ if (c.dependencies) {
137
+ c.dependencies.forEach(dep => {
138
+ const depName = normalizeName(dep);
139
+ if (!reverseGraph.has(depName)) reverseGraph.set(depName, []);
140
+ reverseGraph.get(depName).push(parentName);
141
+ });
142
+ }
143
+ });
144
+
106
145
  // Main Report Header
107
146
  const reportHeader = {
108
147
  buildId,
@@ -153,7 +192,7 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
153
192
  impossible: [], // Missing Data (Historical) / Impossible Dependency
154
193
  uptodate: [], // Hash Match (Previously "Skipped")
155
194
 
156
- // [NEW] Metadata for Verification
195
+ // Metadata for Verification
157
196
  meta: {
158
197
  totalIncluded: 0,
159
198
  totalExpected: 0,
@@ -166,18 +205,24 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
166
205
  dateSummary.meta.totalExpected = expectedCount;
167
206
 
168
207
  // Helper to push only if date is valid for this specific calc
169
- // [UPDATED] Adds 'pass' number to the record
170
208
  const pushIfValid = (targetArray, item, extraReason = null) => {
171
209
  const calcManifest = manifestMap.get(item.name);
172
210
  if (calcManifest && isDateBeforeAvailability(dateStr, calcManifest)) {
173
211
  return; // EXCLUDED: Date is before data exists
174
212
  }
175
213
 
176
- targetArray.push({
214
+ const entry = {
177
215
  name: item.name,
178
216
  reason: item.reason || extraReason,
179
217
  pass: calcManifest ? calcManifest.pass : '?'
180
- });
218
+ };
219
+
220
+ // [IDEA 1] If this is a Re-Run, calculate Blast Radius
221
+ if (targetArray === dateSummary.rerun) {
222
+ entry.impact = calculateBlastRadius(item.name, reverseGraph);
223
+ }
224
+
225
+ targetArray.push(entry);
181
226
  };
182
227
 
183
228
  // 1. RUN (New)
@@ -187,7 +232,6 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
187
232
  analysis.reRuns.forEach(item => pushIfValid(dateSummary.rerun, item, "Hash Mismatch"));
188
233
 
189
234
  // 3. BLOCKED (Temporary Issues)
190
- // Merging 'blocked' and 'failedDependency' as both are temporary blocks
191
235
  analysis.blocked.forEach(item => pushIfValid(dateSummary.blocked, item));
192
236
  analysis.failedDependency.forEach(item => pushIfValid(dateSummary.blocked, item, "Dependency Missing"));
193
237
 
@@ -211,7 +255,7 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
211
255
  logger.log('WARN', `[BuildReporter] ⚠️ Mismatch on ${dateStr}: Expected ${expectedCount} but got ${includedCount}.`);
212
256
  }
213
257
 
214
- // ALWAYS WRITE THE REPORT (No filtering based on activity)
258
+ // QUEUE THE WRITE (Don't write yet)
215
259
  const detailRef = db.collection('computation_build_records').doc(buildId).collection('details').doc(dateStr);
216
260
  detailWrites.push({
217
261
  ref: detailRef,
@@ -247,22 +291,33 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
247
291
  const reportRef = db.collection('computation_build_records').doc(buildId);
248
292
  await reportRef.set(reportHeader);
249
293
 
250
- // 2. Write Details (Protected)
251
- // [FIX] We wrap this in try-catch so that if the massive detail write fails,
252
- // we still update the 'latest' pointer to the new version.
294
+ // 2. Write Details (Protected & Parallelized)
295
+ // FIX: Using parallel individual writes instead of Batch to avoid DEADLINE_EXCEEDED
253
296
  let detailsSuccess = true;
254
297
  if (detailWrites.length > 0) {
255
- logger.log('INFO', `[BuildReporter] Writing ${detailWrites.length} detail records...`);
298
+ logger.log('INFO', `[BuildReporter] Writing ${detailWrites.length} detail records (Parallel Strategy)...`);
299
+
256
300
  try {
257
- await commitBatchInChunks(config, dependencies, detailWrites, 'BuildReportDetails');
301
+ // Concurrency limit of 15 to be safe
302
+ const writeLimit = pLimit(15);
303
+ const writePromises = detailWrites.map(w => writeLimit(() =>
304
+ w.ref.set(w.data).catch(e => {
305
+ logger.log('WARN', `[BuildReporter] Failed to write detail for ${w.ref.path}: ${e.message}`);
306
+ throw e;
307
+ })
308
+ ));
309
+
310
+ await Promise.all(writePromises);
311
+ logger.log('INFO', `[BuildReporter] Successfully wrote all detail records.`);
312
+
258
313
  } catch (detailErr) {
259
314
  detailsSuccess = false;
260
- logger.log('ERROR', `[BuildReporter] ⚠️ Failed to write all details, but Report Header is saved.`, detailErr);
315
+ logger.log('ERROR', `[BuildReporter] ⚠️ Failed to write some details. Report Header is preserved.`, detailErr);
261
316
  }
262
317
  }
263
318
 
264
319
  // 3. Update 'latest' pointer
265
- // This now runs even if details failed, preventing the version mismatch bug.
320
+ // This runs regardless of detail write success/failure
266
321
  const latestMetadata = {
267
322
  ...reportHeader,
268
323
  note: detailsSuccess
@@ -270,9 +325,12 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
270
325
  : "Latest build report pointer (WARNING: Partial detail records due to write error)."
271
326
  };
272
327
 
273
- await db.collection('computation_build_records').doc('latest').set(latestMetadata);
274
-
275
- logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, New: ${totalRun}.`);
328
+ try {
329
+ await db.collection('computation_build_records').doc('latest').set(latestMetadata);
330
+ logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, New: ${totalRun}. Pointer Updated.`);
331
+ } catch (pointerErr) {
332
+ logger.log('FATAL', `[BuildReporter] Failed to update 'latest' pointer!`, pointerErr);
333
+ }
276
334
 
277
335
  return {
278
336
  success: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.278",
3
+ "version": "1.0.280",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [