retold-facto 0.0.4
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.
- package/.claude/launch.json +11 -0
- package/.dockerignore +8 -0
- package/.quackage.json +19 -0
- package/Dockerfile +26 -0
- package/bin/retold-facto.js +909 -0
- package/examples/facto-government-data.sqlite +0 -0
- package/examples/government-data-catalog.json +137 -0
- package/examples/government-data-loader.js +1432 -0
- package/package.json +91 -0
- package/scripts/facto-download.js +425 -0
- package/source/Retold-Facto.js +1042 -0
- package/source/services/Retold-Facto-BeaconProvider.js +511 -0
- package/source/services/Retold-Facto-CatalogManager.js +1252 -0
- package/source/services/Retold-Facto-DataLakeService.js +1642 -0
- package/source/services/Retold-Facto-DatasetManager.js +417 -0
- package/source/services/Retold-Facto-IngestEngine.js +1315 -0
- package/source/services/Retold-Facto-ProjectionEngine.js +3960 -0
- package/source/services/Retold-Facto-RecordManager.js +360 -0
- package/source/services/Retold-Facto-SchemaManager.js +1110 -0
- package/source/services/Retold-Facto-SourceFolderScanner.js +2243 -0
- package/source/services/Retold-Facto-SourceManager.js +730 -0
- package/source/services/Retold-Facto-StoreConnectionManager.js +441 -0
- package/source/services/Retold-Facto-ThroughputMonitor.js +478 -0
- package/source/services/web-app/codemirror-entry.js +7 -0
- package/source/services/web-app/pict-app/Pict-Application-Facto-Configuration.json +9 -0
- package/source/services/web-app/pict-app/Pict-Application-Facto.js +70 -0
- package/source/services/web-app/pict-app/Pict-Facto-Bundle.js +11 -0
- package/source/services/web-app/pict-app/providers/Pict-Provider-Facto-UI.js +66 -0
- package/source/services/web-app/pict-app/providers/Pict-Provider-Facto.js +69 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Catalog.js +93 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Connections.js +42 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Datasets.js +605 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Projections.js +188 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Scanner.js +80 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Schema.js +116 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Sources.js +104 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Catalog.js +526 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Datasets.js +173 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Ingest.js +259 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Layout.js +191 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Projections.js +231 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Records.js +326 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Scanner.js +624 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Sources.js +201 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Throughput.js +456 -0
- package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full-Configuration.json +14 -0
- package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full.js +391 -0
- package/source/services/web-app/pict-app-full/providers/PictRouter-Facto-Configuration.json +56 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-BottomBar.js +68 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Connections.js +340 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboard.js +149 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboards.js +819 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Datasets.js +178 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-IngestJobs.js +99 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Layout.js +62 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-MappingEditor.js +158 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-ProjectionDetail.js +1120 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Projections.js +172 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-QueryPanel.js +119 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-RecordViewer.js +663 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Records.js +648 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Scanner.js +1017 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDetail.js +1404 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDocEditor.js +1036 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaEditor.js +636 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaResearch.js +357 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceDetail.js +822 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceEditor.js +1036 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceResearch.js +487 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Sources.js +165 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Throughput.js +439 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-TopBar.js +335 -0
- package/source/services/web-app/pict-app-full/views/projections/Facto-Projections-Constants.js +71 -0
- package/source/services/web-app/web/chart.min.js +20 -0
- package/source/services/web-app/web/codemirror-bundle.js +30099 -0
- package/source/services/web-app/web/css/facto-themes.css +467 -0
- package/source/services/web-app/web/css/facto.css +502 -0
- package/source/services/web-app/web/index.html +28 -0
- package/source/services/web-app/web/retold-facto.js +12138 -0
- package/source/services/web-app/web/retold-facto.js.map +1 -0
- package/source/services/web-app/web/retold-facto.min.js +2 -0
- package/source/services/web-app/web/retold-facto.min.js.map +1 -0
- package/source/services/web-app/web/simple/index.html +17 -0
- package/test/Facto_Browser_Integration_tests.js +798 -0
- package/test/RetoldFacto_tests.js +4117 -0
- package/test/fixtures/weather-readings.csv +17 -0
- package/test/fixtures/weather-stations.csv +9 -0
- package/test/model/MeadowModel-Extended.json +8497 -0
- package/test/model/MeadowModel-PICT.json +1 -0
- package/test/model/MeadowModel.json +1355 -0
- package/test/model/ddl/Facto.ddl +225 -0
- package/test/model/fable-configuration.json +14 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retold-Facto-ThroughputMonitor
|
|
3
|
+
*
|
|
4
|
+
* In-memory ring buffer that collects timestamped throughput events
|
|
5
|
+
* from pipeline stages (extracted, transformed, written).
|
|
6
|
+
*
|
|
7
|
+
* Events are bucketed into configurable time intervals (default 1s)
|
|
8
|
+
* and exposed via a REST endpoint for the temporal histogram UI.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* fable.ThroughputMonitor.recordEvent('extracted', 50);
|
|
12
|
+
* fable.ThroughputMonitor.recordEvent('transformed', 48);
|
|
13
|
+
* fable.ThroughputMonitor.recordEvent('written', 45);
|
|
14
|
+
*
|
|
15
|
+
* let buckets = fable.ThroughputMonitor.getBuckets(60); // last 60 seconds
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
21
|
+
|
|
22
|
+
const STAGES = ['extracted', 'transformed', 'written'];
|
|
23
|
+
|
|
24
|
+
class RetoldFactoThroughputMonitor extends libFableServiceProviderBase
|
|
25
|
+
{
|
|
26
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
27
|
+
{
|
|
28
|
+
super(pFable, pOptions, pServiceHash);
|
|
29
|
+
|
|
30
|
+
this.serviceType = 'ThroughputMonitor';
|
|
31
|
+
|
|
32
|
+
// Configurable bucket interval in milliseconds (default 1 second)
|
|
33
|
+
this._BucketIntervalMs = (pOptions && pOptions.BucketIntervalMs) || 1000;
|
|
34
|
+
|
|
35
|
+
// Ring buffer of events: { timestamp, stage, count }
|
|
36
|
+
this._Events = [];
|
|
37
|
+
|
|
38
|
+
// Maximum events to keep (prevents unbounded memory growth)
|
|
39
|
+
this._MaxEvents = (pOptions && pOptions.MaxEvents) || 100000;
|
|
40
|
+
|
|
41
|
+
// Pipeline run tracking
|
|
42
|
+
this._ActiveRun = null;
|
|
43
|
+
this._RunHistory = [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─────────────────────────────────────────────
|
|
47
|
+
// Event recording
|
|
48
|
+
// ─────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Record a throughput event.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} pStage — 'extracted', 'transformed', or 'written'
|
|
54
|
+
* @param {number} pCount — number of records in this event
|
|
55
|
+
* @param {string} [pDatasetName] — optional dataset label
|
|
56
|
+
*/
|
|
57
|
+
recordEvent(pStage, pCount, pDatasetName)
|
|
58
|
+
{
|
|
59
|
+
if (STAGES.indexOf(pStage) < 0)
|
|
60
|
+
{
|
|
61
|
+
this.log.warn(`ThroughputMonitor: unknown stage "${pStage}"`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this._Events.push(
|
|
66
|
+
{
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
stage: pStage,
|
|
69
|
+
count: pCount || 0,
|
|
70
|
+
dataset: pDatasetName || '',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Trim ring buffer
|
|
74
|
+
if (this._Events.length > this._MaxEvents)
|
|
75
|
+
{
|
|
76
|
+
this._Events = this._Events.slice(this._Events.length - this._MaxEvents);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─────────────────────────────────────────────
|
|
81
|
+
// Run lifecycle
|
|
82
|
+
// ─────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Mark the start of a pipeline run. Clears previous events.
|
|
86
|
+
*/
|
|
87
|
+
startRun(pRunLabel)
|
|
88
|
+
{
|
|
89
|
+
if (this._ActiveRun)
|
|
90
|
+
{
|
|
91
|
+
this.endRun();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this._Events = [];
|
|
95
|
+
this._ActiveRun =
|
|
96
|
+
{
|
|
97
|
+
label: pRunLabel || 'run-' + Date.now(),
|
|
98
|
+
startTime: Date.now(),
|
|
99
|
+
endTime: null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Mark the end of a pipeline run and flush events to the database.
|
|
105
|
+
*/
|
|
106
|
+
endRun()
|
|
107
|
+
{
|
|
108
|
+
if (this._ActiveRun)
|
|
109
|
+
{
|
|
110
|
+
this._ActiveRun.endTime = Date.now();
|
|
111
|
+
this._RunHistory.push(this._ActiveRun);
|
|
112
|
+
if (this._RunHistory.length > 10)
|
|
113
|
+
{
|
|
114
|
+
this._RunHistory.shift();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Persist events to the ThroughputEvent table
|
|
118
|
+
this._flushToDatabase(this._ActiveRun);
|
|
119
|
+
}
|
|
120
|
+
this._ActiveRun = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Batch-write all buffered events to the ThroughputEvent table.
|
|
125
|
+
* Uses the MeadowSQLiteProvider's better-sqlite3 database handle.
|
|
126
|
+
*/
|
|
127
|
+
_flushToDatabase(pRun)
|
|
128
|
+
{
|
|
129
|
+
if (!this._Events || this._Events.length === 0)
|
|
130
|
+
{
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let tmpDB = this._getDatabase();
|
|
135
|
+
if (!tmpDB)
|
|
136
|
+
{
|
|
137
|
+
this.log.warn('ThroughputMonitor: no database available for persistence');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let tmpLabel = pRun.label || '';
|
|
142
|
+
let tmpStartTime = pRun.startTime || 0;
|
|
143
|
+
|
|
144
|
+
try
|
|
145
|
+
{
|
|
146
|
+
let tmpStmt = tmpDB.prepare(
|
|
147
|
+
'INSERT INTO ThroughputEvent (RunLabel, RunStartTime, Timestamp, Stage, Count, Dataset) VALUES (?, ?, ?, ?, ?, ?)');
|
|
148
|
+
|
|
149
|
+
let tmpInsertMany = tmpDB.transaction(
|
|
150
|
+
(pEvents) =>
|
|
151
|
+
{
|
|
152
|
+
for (let i = 0; i < pEvents.length; i++)
|
|
153
|
+
{
|
|
154
|
+
let tmpEvent = pEvents[i];
|
|
155
|
+
tmpStmt.run(tmpLabel, tmpStartTime, tmpEvent.timestamp, tmpEvent.stage, tmpEvent.count, tmpEvent.dataset || '');
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
tmpInsertMany(this._Events);
|
|
160
|
+
this.log.info(`ThroughputMonitor: flushed ${this._Events.length} events for run "${tmpLabel}"`);
|
|
161
|
+
}
|
|
162
|
+
catch (pError)
|
|
163
|
+
{
|
|
164
|
+
this.log.warn(`ThroughputMonitor: flush error: ${pError.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get the better-sqlite3 database handle from the MeadowSQLiteProvider.
|
|
170
|
+
*/
|
|
171
|
+
_getDatabase()
|
|
172
|
+
{
|
|
173
|
+
if (!this.fable.servicesMap || !this.fable.servicesMap.MeadowSQLiteProvider)
|
|
174
|
+
{
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
let tmpProvider = Object.values(this.fable.servicesMap.MeadowSQLiteProvider)[0];
|
|
178
|
+
return tmpProvider && tmpProvider._database ? tmpProvider._database : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Query persisted runs from the database.
|
|
183
|
+
*
|
|
184
|
+
* @param {number} [pLimit=20] — max runs to return
|
|
185
|
+
* @returns {Array} — [{ label, startTime, eventCount, datasets }]
|
|
186
|
+
*/
|
|
187
|
+
getPersistedRuns(pLimit)
|
|
188
|
+
{
|
|
189
|
+
let tmpDB = this._getDatabase();
|
|
190
|
+
if (!tmpDB)
|
|
191
|
+
{
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let tmpLimit = pLimit || 20;
|
|
196
|
+
try
|
|
197
|
+
{
|
|
198
|
+
let tmpRows = tmpDB.prepare(
|
|
199
|
+
`SELECT RunLabel, RunStartTime, COUNT(*) as EventCount, GROUP_CONCAT(DISTINCT Dataset) as Datasets
|
|
200
|
+
FROM ThroughputEvent
|
|
201
|
+
GROUP BY RunLabel, RunStartTime
|
|
202
|
+
ORDER BY RunStartTime DESC
|
|
203
|
+
LIMIT ?`).all(tmpLimit);
|
|
204
|
+
|
|
205
|
+
return tmpRows.map(
|
|
206
|
+
(pRow) =>
|
|
207
|
+
({
|
|
208
|
+
label: pRow.RunLabel,
|
|
209
|
+
startTime: pRow.RunStartTime,
|
|
210
|
+
eventCount: pRow.EventCount,
|
|
211
|
+
datasets: (pRow.Datasets || '').split(',').filter((pD) => pD.length > 0),
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
catch (pError)
|
|
215
|
+
{
|
|
216
|
+
this.log.warn(`ThroughputMonitor: query error: ${pError.message}`);
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Load events for a specific historical run, optionally filtered by dataset.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} pRunLabel — run label to load
|
|
225
|
+
* @param {string} [pDataset] — optional dataset filter
|
|
226
|
+
* @returns {object} — same format as getBuckets()
|
|
227
|
+
*/
|
|
228
|
+
getPersistedRunBuckets(pRunLabel, pDataset)
|
|
229
|
+
{
|
|
230
|
+
let tmpDB = this._getDatabase();
|
|
231
|
+
if (!tmpDB)
|
|
232
|
+
{
|
|
233
|
+
return { buckets: [], stages: STAGES, interval: this._BucketIntervalMs, maxValue: 0, datasets: [] };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try
|
|
237
|
+
{
|
|
238
|
+
let tmpParams = [pRunLabel || ''];
|
|
239
|
+
let tmpSql = 'SELECT Timestamp, Stage, Count, Dataset FROM ThroughputEvent WHERE RunLabel = ?';
|
|
240
|
+
if (pDataset)
|
|
241
|
+
{
|
|
242
|
+
tmpSql += ' AND Dataset = ?';
|
|
243
|
+
tmpParams.push(pDataset);
|
|
244
|
+
}
|
|
245
|
+
tmpSql += ' ORDER BY Timestamp';
|
|
246
|
+
|
|
247
|
+
let tmpRows = tmpDB.prepare(tmpSql).all(...tmpParams);
|
|
248
|
+
|
|
249
|
+
if (!tmpRows || tmpRows.length === 0)
|
|
250
|
+
{
|
|
251
|
+
return { buckets: [], stages: STAGES, interval: this._BucketIntervalMs, maxValue: 0, datasets: [] };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Reconstruct events
|
|
255
|
+
let tmpEvents = tmpRows.map(
|
|
256
|
+
(pRow) => ({ timestamp: pRow.Timestamp, stage: pRow.Stage, count: pRow.Count, dataset: pRow.Dataset }));
|
|
257
|
+
|
|
258
|
+
// Find time range
|
|
259
|
+
let tmpMinTime = tmpEvents[0].timestamp;
|
|
260
|
+
let tmpMaxTime = tmpEvents[tmpEvents.length - 1].timestamp;
|
|
261
|
+
let tmpDuration = tmpMaxTime - tmpMinTime + this._BucketIntervalMs;
|
|
262
|
+
let tmpInterval = this._BucketIntervalMs;
|
|
263
|
+
let tmpBucketCount = Math.ceil(tmpDuration / tmpInterval);
|
|
264
|
+
|
|
265
|
+
let tmpBuckets = [];
|
|
266
|
+
for (let i = 0; i < tmpBucketCount; i++)
|
|
267
|
+
{
|
|
268
|
+
tmpBuckets.push(
|
|
269
|
+
{
|
|
270
|
+
time: tmpMinTime + (i * tmpInterval),
|
|
271
|
+
extracted: 0,
|
|
272
|
+
transformed: 0,
|
|
273
|
+
written: 0,
|
|
274
|
+
total: 0,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Collect unique datasets
|
|
279
|
+
let tmpDatasetSet = {};
|
|
280
|
+
|
|
281
|
+
for (let i = 0; i < tmpEvents.length; i++)
|
|
282
|
+
{
|
|
283
|
+
let tmpEvent = tmpEvents[i];
|
|
284
|
+
let tmpBucketIndex = Math.floor((tmpEvent.timestamp - tmpMinTime) / tmpInterval);
|
|
285
|
+
if (tmpBucketIndex >= 0 && tmpBucketIndex < tmpBuckets.length)
|
|
286
|
+
{
|
|
287
|
+
tmpBuckets[tmpBucketIndex][tmpEvent.stage] += tmpEvent.count;
|
|
288
|
+
tmpBuckets[tmpBucketIndex].total += tmpEvent.count;
|
|
289
|
+
}
|
|
290
|
+
if (tmpEvent.dataset)
|
|
291
|
+
{
|
|
292
|
+
tmpDatasetSet[tmpEvent.dataset] = true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let tmpMaxValue = 0;
|
|
297
|
+
for (let i = 0; i < tmpBuckets.length; i++)
|
|
298
|
+
{
|
|
299
|
+
tmpMaxValue = Math.max(tmpMaxValue, tmpBuckets[i].extracted, tmpBuckets[i].transformed, tmpBuckets[i].written);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
buckets: tmpBuckets,
|
|
304
|
+
stages: STAGES,
|
|
305
|
+
interval: tmpInterval,
|
|
306
|
+
maxValue: tmpMaxValue,
|
|
307
|
+
datasets: Object.keys(tmpDatasetSet),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
catch (pError)
|
|
311
|
+
{
|
|
312
|
+
this.log.warn(`ThroughputMonitor: query error: ${pError.message}`);
|
|
313
|
+
return { buckets: [], stages: STAGES, interval: this._BucketIntervalMs, maxValue: 0, datasets: [] };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get per-dataset breakdown for a run.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} pRunLabel — run label
|
|
321
|
+
* @returns {Array} — [{ dataset, extracted, transformed, written, total }]
|
|
322
|
+
*/
|
|
323
|
+
getPersistedRunDatasetBreakdown(pRunLabel)
|
|
324
|
+
{
|
|
325
|
+
let tmpDB = this._getDatabase();
|
|
326
|
+
if (!tmpDB)
|
|
327
|
+
{
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try
|
|
332
|
+
{
|
|
333
|
+
let tmpRows = tmpDB.prepare(
|
|
334
|
+
`SELECT Dataset, Stage, SUM(Count) as Total
|
|
335
|
+
FROM ThroughputEvent
|
|
336
|
+
WHERE RunLabel = ? AND Dataset != ''
|
|
337
|
+
GROUP BY Dataset, Stage
|
|
338
|
+
ORDER BY Dataset, Stage`).all(pRunLabel || '');
|
|
339
|
+
|
|
340
|
+
if (!tmpRows || tmpRows.length === 0) return [];
|
|
341
|
+
|
|
342
|
+
// Pivot: rows are (Dataset, Stage, Total) → objects are { dataset, extracted, transformed, written }
|
|
343
|
+
let tmpMap = {};
|
|
344
|
+
for (let i = 0; i < tmpRows.length; i++)
|
|
345
|
+
{
|
|
346
|
+
let tmpRow = tmpRows[i];
|
|
347
|
+
let tmpDataset = tmpRow.Dataset;
|
|
348
|
+
let tmpStage = tmpRow.Stage;
|
|
349
|
+
let tmpTotal = tmpRow.Total;
|
|
350
|
+
|
|
351
|
+
if (!tmpMap[tmpDataset])
|
|
352
|
+
{
|
|
353
|
+
tmpMap[tmpDataset] = { dataset: tmpDataset, extracted: 0, transformed: 0, written: 0, total: 0 };
|
|
354
|
+
}
|
|
355
|
+
tmpMap[tmpDataset][tmpStage] = tmpTotal;
|
|
356
|
+
tmpMap[tmpDataset].total += tmpTotal;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return Object.values(tmpMap);
|
|
360
|
+
}
|
|
361
|
+
catch (pError)
|
|
362
|
+
{
|
|
363
|
+
this.log.warn(`ThroughputMonitor: breakdown error: ${pError.message}`);
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ─────────────────────────────────────────────
|
|
369
|
+
// Bucketed query
|
|
370
|
+
// ─────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get time-bucketed throughput data.
|
|
374
|
+
*
|
|
375
|
+
* @param {number} [pDurationSeconds=60] — how far back to look
|
|
376
|
+
* @returns {object} — { buckets: [...], stages, interval, activeRun }
|
|
377
|
+
*/
|
|
378
|
+
getBuckets(pDurationSeconds)
|
|
379
|
+
{
|
|
380
|
+
let tmpDuration = (pDurationSeconds || 60) * 1000;
|
|
381
|
+
let tmpNow = Date.now();
|
|
382
|
+
let tmpInterval = this._BucketIntervalMs;
|
|
383
|
+
|
|
384
|
+
// Determine the right-edge anchor for the window:
|
|
385
|
+
// Active run → anchor to "now" so live events always appear at the right edge.
|
|
386
|
+
// No active run → anchor to the last recorded event so bars stay right-aligned
|
|
387
|
+
// and don't float into dead space on the right as time passes.
|
|
388
|
+
let tmpAnchor = tmpNow;
|
|
389
|
+
if (!this._ActiveRun && this._Events.length > 0)
|
|
390
|
+
{
|
|
391
|
+
tmpAnchor = this._Events[this._Events.length - 1].timestamp;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Align to a clock-boundary so bucket indices are stable across consecutive
|
|
395
|
+
// polls — the same event always lands in the same bucket, no rebucketing jitter.
|
|
396
|
+
let tmpAlignedAnchor = Math.floor(tmpAnchor / tmpInterval) * tmpInterval;
|
|
397
|
+
let tmpBucketCount = Math.ceil(tmpDuration / tmpInterval);
|
|
398
|
+
let tmpStart = tmpAlignedAnchor - (tmpBucketCount - 1) * tmpInterval;
|
|
399
|
+
let tmpBuckets = [];
|
|
400
|
+
|
|
401
|
+
for (let i = 0; i < tmpBucketCount; i++)
|
|
402
|
+
{
|
|
403
|
+
let tmpBucketStart = tmpStart + (i * tmpInterval);
|
|
404
|
+
tmpBuckets.push(
|
|
405
|
+
{
|
|
406
|
+
time: tmpBucketStart,
|
|
407
|
+
extracted: 0,
|
|
408
|
+
transformed: 0,
|
|
409
|
+
written: 0,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Fill buckets from events
|
|
414
|
+
for (let i = 0; i < this._Events.length; i++)
|
|
415
|
+
{
|
|
416
|
+
let tmpEvent = this._Events[i];
|
|
417
|
+
if (tmpEvent.timestamp < tmpStart)
|
|
418
|
+
{
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let tmpBucketIndex = Math.min(Math.floor((tmpEvent.timestamp - tmpStart) / tmpInterval), tmpBuckets.length - 1);
|
|
423
|
+
if (tmpBucketIndex >= 0 && tmpBucketIndex < tmpBuckets.length)
|
|
424
|
+
{
|
|
425
|
+
tmpBuckets[tmpBucketIndex][tmpEvent.stage] += tmpEvent.count;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Find max value for scaling
|
|
430
|
+
let tmpMaxValue = 0;
|
|
431
|
+
for (let i = 0; i < tmpBuckets.length; i++)
|
|
432
|
+
{
|
|
433
|
+
let tmpBucket = tmpBuckets[i];
|
|
434
|
+
tmpMaxValue = Math.max(tmpMaxValue, tmpBucket.extracted, tmpBucket.transformed, tmpBucket.written);
|
|
435
|
+
// Also track stacked total
|
|
436
|
+
tmpBucket.total = tmpBucket.extracted + tmpBucket.transformed + tmpBucket.written;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
buckets: tmpBuckets,
|
|
441
|
+
stages: STAGES,
|
|
442
|
+
interval: tmpInterval,
|
|
443
|
+
maxValue: tmpMaxValue,
|
|
444
|
+
activeRun: this._ActiveRun,
|
|
445
|
+
totalEvents: this._Events.length,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get cumulative totals per stage.
|
|
451
|
+
*/
|
|
452
|
+
getTotals()
|
|
453
|
+
{
|
|
454
|
+
let tmpTotals = { extracted: 0, transformed: 0, written: 0 };
|
|
455
|
+
|
|
456
|
+
for (let i = 0; i < this._Events.length; i++)
|
|
457
|
+
{
|
|
458
|
+
let tmpEvent = this._Events[i];
|
|
459
|
+
if (tmpTotals.hasOwnProperty(tmpEvent.stage))
|
|
460
|
+
{
|
|
461
|
+
tmpTotals[tmpEvent.stage] += tmpEvent.count;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return tmpTotals;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Clear all events.
|
|
470
|
+
*/
|
|
471
|
+
reset()
|
|
472
|
+
{
|
|
473
|
+
this._Events = [];
|
|
474
|
+
this._ActiveRun = null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
module.exports = RetoldFactoThroughputMonitor;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { EditorView, basicSetup } from 'codemirror';
|
|
2
|
+
import { EditorState } from '@codemirror/state';
|
|
3
|
+
import { Decoration, ViewPlugin, WidgetType } from '@codemirror/view';
|
|
4
|
+
import { markdown } from '@codemirror/lang-markdown';
|
|
5
|
+
|
|
6
|
+
export { EditorView, EditorState, Decoration, ViewPlugin, WidgetType, basicSetup, markdown };
|
|
7
|
+
export const extensions = [basicSetup, markdown()];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Name": "Retold Facto",
|
|
3
|
+
"Hash": "Facto",
|
|
4
|
+
"MainViewportViewIdentifier": "Facto-Layout",
|
|
5
|
+
"MainViewportDestinationAddress": "#Facto-Application-Container",
|
|
6
|
+
"MainViewportDefaultDataAddress": "AppData.Facto",
|
|
7
|
+
"pict_configuration": { "Product": "Facto" },
|
|
8
|
+
"AutoRenderMainViewportViewAfterInitialize": false
|
|
9
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const libPictApplication = require('pict-application');
|
|
2
|
+
|
|
3
|
+
const libPictSectionModal = require('pict-section-modal');
|
|
4
|
+
const libProvider = require('./providers/Pict-Provider-Facto.js');
|
|
5
|
+
|
|
6
|
+
const libViewLayout = require('./views/PictView-Facto-Layout.js');
|
|
7
|
+
const libViewSources = require('./views/PictView-Facto-Sources.js');
|
|
8
|
+
const libViewRecords = require('./views/PictView-Facto-Records.js');
|
|
9
|
+
const libViewDatasets = require('./views/PictView-Facto-Datasets.js');
|
|
10
|
+
const libViewIngest = require('./views/PictView-Facto-Ingest.js');
|
|
11
|
+
const libViewProjections = require('./views/PictView-Facto-Projections.js');
|
|
12
|
+
const libViewCatalog = require('./views/PictView-Facto-Catalog.js');
|
|
13
|
+
const libViewScanner = require('./views/PictView-Facto-Scanner.js');
|
|
14
|
+
const libViewThroughput = require('./views/PictView-Facto-Throughput.js');
|
|
15
|
+
|
|
16
|
+
class FactoApplication extends libPictApplication
|
|
17
|
+
{
|
|
18
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
19
|
+
{
|
|
20
|
+
super(pFable, pOptions, pServiceHash);
|
|
21
|
+
|
|
22
|
+
// Register modal/notification service
|
|
23
|
+
this.pict.addView('Pict-Section-Modal', libPictSectionModal.default_configuration, libPictSectionModal);
|
|
24
|
+
|
|
25
|
+
// Register provider
|
|
26
|
+
this.pict.addProvider('Facto', libProvider.default_configuration, libProvider);
|
|
27
|
+
|
|
28
|
+
// Register views
|
|
29
|
+
this.pict.addView('Facto-Layout', libViewLayout.default_configuration, libViewLayout);
|
|
30
|
+
this.pict.addView('Facto-Sources', libViewSources.default_configuration, libViewSources);
|
|
31
|
+
this.pict.addView('Facto-Records', libViewRecords.default_configuration, libViewRecords);
|
|
32
|
+
this.pict.addView('Facto-Datasets', libViewDatasets.default_configuration, libViewDatasets);
|
|
33
|
+
this.pict.addView('Facto-Ingest', libViewIngest.default_configuration, libViewIngest);
|
|
34
|
+
this.pict.addView('Facto-Projections', libViewProjections.default_configuration, libViewProjections);
|
|
35
|
+
this.pict.addView('Facto-Catalog', libViewCatalog.default_configuration, libViewCatalog);
|
|
36
|
+
this.pict.addView('Facto-Scanner', libViewScanner.default_configuration, libViewScanner);
|
|
37
|
+
this.pict.addView('Facto-Throughput', libViewThroughput.default_configuration, libViewThroughput);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onAfterInitializeAsync(fCallback)
|
|
41
|
+
{
|
|
42
|
+
// Centralized application state
|
|
43
|
+
this.pict.AppData.Facto =
|
|
44
|
+
{
|
|
45
|
+
CatalogEntries: [],
|
|
46
|
+
Sources: [],
|
|
47
|
+
Datasets: [],
|
|
48
|
+
Records: [],
|
|
49
|
+
IngestJobs: [],
|
|
50
|
+
SelectedSource: null,
|
|
51
|
+
SelectedDataset: null,
|
|
52
|
+
RecordPage: 0,
|
|
53
|
+
RecordPageSize: 50,
|
|
54
|
+
ScannerPaths: [],
|
|
55
|
+
ScannerDatasets: []
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Make pict available for inline onclick handlers
|
|
59
|
+
window.pict = this.pict;
|
|
60
|
+
|
|
61
|
+
// Render layout (which cascades child view renders)
|
|
62
|
+
this.pict.views['Facto-Layout'].render();
|
|
63
|
+
|
|
64
|
+
return fCallback();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = FactoApplication;
|
|
69
|
+
|
|
70
|
+
module.exports.default_configuration = require('./Pict-Application-Facto-Configuration.json');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module.exports =
|
|
2
|
+
{
|
|
3
|
+
FactoApplication: require('./Pict-Application-Facto.js'),
|
|
4
|
+
FactoFullApplication: require('../pict-app-full/Pict-Application-Facto-Full.js')
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
if (typeof(window) !== 'undefined')
|
|
8
|
+
{
|
|
9
|
+
window.FactoApplication = module.exports.FactoApplication;
|
|
10
|
+
window.FactoFullApplication = module.exports.FactoFullApplication;
|
|
11
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const libPictProvider = require('pict-provider');
|
|
2
|
+
|
|
3
|
+
const _ProviderConfiguration =
|
|
4
|
+
{
|
|
5
|
+
ProviderIdentifier: 'FactoUI',
|
|
6
|
+
AutoInitialize: true,
|
|
7
|
+
AutoInitializeOrdinal: 0
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
class FactoUIProvider extends libPictProvider
|
|
11
|
+
{
|
|
12
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
13
|
+
{
|
|
14
|
+
super(pFable, pOptions, pServiceHash);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Read a form field value by element ID using pict's ContentAssignment.
|
|
19
|
+
* Replaces the (document.getElementById(id) || {}).value || '' pattern.
|
|
20
|
+
* @param {string} pID - The element ID (without the leading #)
|
|
21
|
+
* @returns {string} The element's value, or empty string if not found
|
|
22
|
+
*/
|
|
23
|
+
getVal(pID)
|
|
24
|
+
{
|
|
25
|
+
return this.pict.ContentAssignment.readContent('#' + pID) || '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reload and refresh sibling views after data-mutating operations.
|
|
30
|
+
* Handles both pict-app ('Facto-X') and pict-app-full ('Facto-Full-X') identifiers.
|
|
31
|
+
* @param {string[]} [pWhich=['sources','datasets']] - Which views to refresh
|
|
32
|
+
*/
|
|
33
|
+
refreshDataViews(pWhich)
|
|
34
|
+
{
|
|
35
|
+
let tmpLoad = pWhich || ['sources', 'datasets'];
|
|
36
|
+
|
|
37
|
+
if (tmpLoad.indexOf('sources') > -1)
|
|
38
|
+
{
|
|
39
|
+
let tmpView = this.pict.views['Facto-Sources'] || this.pict.views['Facto-Full-Sources'];
|
|
40
|
+
if (tmpView)
|
|
41
|
+
{
|
|
42
|
+
this.pict.providers.Facto.loadSources().then(() => { tmpView.refreshList(); });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (tmpLoad.indexOf('datasets') > -1)
|
|
46
|
+
{
|
|
47
|
+
let tmpView = this.pict.views['Facto-Datasets'] || this.pict.views['Facto-Full-Datasets'];
|
|
48
|
+
if (tmpView)
|
|
49
|
+
{
|
|
50
|
+
this.pict.providers.Facto.loadDatasets().then(() => { tmpView.refreshList(); });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (tmpLoad.indexOf('records') > -1)
|
|
54
|
+
{
|
|
55
|
+
let tmpView = this.pict.views['Facto-Records'] || this.pict.views['Facto-Full-Records'];
|
|
56
|
+
if (tmpView)
|
|
57
|
+
{
|
|
58
|
+
this.pict.providers.Facto.loadRecords().then(() => { tmpView.refreshList(); });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = FactoUIProvider;
|
|
65
|
+
|
|
66
|
+
module.exports.default_configuration = _ProviderConfiguration;
|