Philip Kehl commited on
Commit
6b11ecf
Β·
2 Parent(s): b9991b1 6c50dd9

Merge branch 'main' of github.com:fabianHueni/UNIBE_MSGAI_HS25_Project

Browse files
Files changed (5) hide show
  1. index.html +2 -5
  2. src/main.js +36 -59
  3. src/requestManager.js +3 -3
  4. src/scheduler.js +41 -23
  5. src/utils.js +13 -1
index.html CHANGED
@@ -129,7 +129,8 @@
129
  class="w-full h-64 overflow-scroll bg-gray-50 p-3 rounded-lg border border-gray-200 text-sm">
130
  <thead>
131
  <tr>
132
- <th class="text-left">Timestamp</th>
 
133
  <th class="text-left">Route</th>
134
  <th class="text-left">Total Latency (ms)</th>
135
  <th class="text-left">Queue (ms)</th>
@@ -145,10 +146,6 @@
145
 
146
  <div id="stats" class="mt-4 text-sm text-gray-800"></div>
147
  <div class="flex flex-col md:flex-row gap-4 mt-4">
148
- <button id="downloadStatsJson"
149
- class="mt-4 w-full bg-purple-600 text-white py-2 rounded-lg hover:bg-purple-700 transition">
150
- Download Statistics as JSON
151
- </button>
152
  <button id="downloadStatsCsv"
153
  class="mt-4 w-full bg-purple-600 text-white py-2 rounded-lg hover:bg-purple-700 transition">
154
  Download Statistics as CSV
 
129
  class="w-full h-64 overflow-scroll bg-gray-50 p-3 rounded-lg border border-gray-200 text-sm">
130
  <thead>
131
  <tr>
132
+ <th class="text-left">ID</th>
133
+ <th class="text-left">Time</th>
134
  <th class="text-left">Route</th>
135
  <th class="text-left">Total Latency (ms)</th>
136
  <th class="text-left">Queue (ms)</th>
 
146
 
147
  <div id="stats" class="mt-4 text-sm text-gray-800"></div>
148
  <div class="flex flex-col md:flex-row gap-4 mt-4">
 
 
 
 
149
  <button id="downloadStatsCsv"
150
  class="mt-4 w-full bg-purple-600 text-white py-2 rounded-lg hover:bg-purple-700 transition">
151
  Download Statistics as CSV
src/main.js CHANGED
@@ -3,7 +3,7 @@ import {RequestManager} from './requestManager.js';
3
  import {OnDeviceService} from './services/onDeviceService.js';
4
  import {CloudService} from './services/cloudService.js';
5
  import {Evaluator} from './evaluator.js';
6
- import {logTo, sleep} from './utils.js';
7
 
8
 
9
  // get references to html elements
@@ -13,7 +13,6 @@ const deviceStatusEl = document.getElementById('deviceStatus');
13
 
14
 
15
  // instantiate services and components
16
- console.log(getModelSelection())
17
  const onDeviceInferenceService = new OnDeviceService(getModelSelection());
18
  const cloudInferenceService = new CloudService({
19
  apiKey: document.getElementById('cloudApiKey').value,
@@ -30,7 +29,7 @@ const requestManager = new RequestManager({
30
  });
31
 
32
 
33
- // instantiate the job scheduler with some mock prompts TODO: replace with real prompts
34
  const scheduler = new JobScheduler('boolq_validation');
35
 
36
 
@@ -78,13 +77,11 @@ document.getElementById('startBtn').addEventListener('click', async () => {
78
 
79
  document.getElementById('stopBtn').addEventListener('click', () => {
80
  scheduler.stop();
 
81
  document.getElementById('startBtn').disabled = false;
82
  document.getElementById('stopBtn').disabled = true;
83
  });
84
 
85
- document.getElementById('downloadStatsJson').addEventListener('click', () => {
86
- downloadStatsAsJson();
87
- });
88
  document.getElementById('downloadStatsCsv').addEventListener('click', () => {
89
  downloadStatsAsCSV();
90
  });
@@ -103,7 +100,7 @@ let currentExperiment = null;
103
  let experimentJobCount = 0;
104
  let experimentTargetJobs = 0;
105
  let isExperimentRunning = false;
106
- const TARGET_JOBS = 1000;
107
 
108
  document.getElementById('start1000Btn').addEventListener('click', async () => {
109
 
@@ -134,7 +131,7 @@ document.getElementById('start1000Btn').addEventListener('click', async () => {
134
  cloudModel,
135
  routeStrategy,
136
  pattern,
137
- startTime: new Date().toISOString()
138
  };
139
 
140
  experimentJobCount = 0;
@@ -183,8 +180,6 @@ document.getElementById('start1000Btn').addEventListener('click', async () => {
183
  });
184
 
185
  function finishExperiment() {
186
- if (!isExperimentRunning) return;
187
-
188
  isExperimentRunning = false;
189
  console.log('βœ… Experiment complete!');
190
 
@@ -222,7 +217,7 @@ function downloadExperimentResults() {
222
  const stats = {
223
  experiment: {
224
  ...currentExperiment,
225
- endTime: new Date().toISOString(),
226
  completedJobs: requestManager.stats.count
227
  },
228
  stats: requestManager.stats
@@ -257,12 +252,12 @@ function buildExperimentCSV(stats) {
257
  const lines = [];
258
 
259
  // Header
260
- lines.push('job_id,route,latency_ms,total_latency_ms,queueing_time_ms,inference_time_ms,exact_match,f1_score,ground_truth,answer');
261
 
262
  // Data rows
263
  stats.stats.results.forEach((result, index) => {
264
  const row = [
265
- index,
266
  result.route || '',
267
  (result.latency || 0).toFixed(2),
268
  (result.totalLatency || 0).toFixed(2),
@@ -271,7 +266,19 @@ function buildExperimentCSV(stats) {
271
  result.evalRes?.exactMatch || false,
272
  (result.evalRes?.f1WordLevel || 0).toFixed(4),
273
  `"${(result.job?.groundTruth || '').replace(/"/g, '""')}"`,
274
- `"${(result.text?.answer || '').replace(/"/g, '""')}"`
 
 
 
 
 
 
 
 
 
 
 
 
275
  ];
276
  lines.push(row.join(','));
277
  });
@@ -403,53 +410,23 @@ async function loadDeviceModel() {
403
  }
404
  }
405
 
406
- function downloadStatsAsJson() {
407
- const s = requestManager.stats;
408
- // add average latency to stats for device and cloud
409
- s.avgLatencyMs = s.count ? (s.totalLatencyMs / s.count) : 0;
410
- s.avgDeviceLatencyMs = s.device ? (s.results.filter(e => e.route === 'device').reduce((a, b) => a + b.latency, 0) / s.device) : 0;
411
- s.avgCloudLatencyMs = s.cloud ? (s.results.filter(e => e.route === 'cloud').reduce((a, b) => a + b.latency, 0) / s.cloud) : 0;
412
-
413
- const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(s, null, 2));
414
- const dlAnchorElem = document.createElement('a');
415
- dlAnchorElem.setAttribute("href", dataStr);
416
- dlAnchorElem.setAttribute("download", "stats.json");
417
- dlAnchorElem.click();
418
- }
419
-
420
  function downloadStatsAsCSV() {
421
- const s = requestManager.stats;
422
 
423
- const flattened_evals = s.results.map(evaluation => ({
424
- route: evaluation.route,
425
- latency: evaluation.latency,
426
- totalLatency: evaluation.totalLatency || 0,
427
- queueingTime: evaluation.queueingTime || 0,
428
- inferenceTime: evaluation.inferenceTime || 0,
429
- prompt: evaluation.job.prompt,
430
-
431
- // job details
432
- groundTruth: evaluation.job.groundTruth,
433
- answer: evaluation.text.answer,
434
-
435
- // evaluation results
436
- exactMatch: evaluation.evalRes.exactMatch,
437
- f1: evaluation.evalRes.f1WordLevel,
438
- tokensPerSecond: evaluation.evalRes.tokensPerSecond,
439
- totalTokens: evaluation.evalRes.totalTokens,
440
-
441
- // further stats
442
- input_tokens: evaluation.text.stats.input_tokens,
443
- output_tokens: evaluation.text.stats.output_tokens,
444
- })
445
- );
446
-
447
- // Convert stats to CSV format
448
- const headers = Object.keys(flattened_evals[0] || {}).join(',');
449
- const rows = flattened_evals.map(evaluation =>
450
- Object.values(evaluation).map(value => `"${value}"`).join(',')
451
- );
452
- const csvContent = [headers, ...rows].join('\n');
453
 
454
  const dataStr = "data:text/csv;charset=utf-8," + encodeURIComponent(csvContent);
455
  const dlAnchorElem = document.createElement('a');
 
3
  import {OnDeviceService} from './services/onDeviceService.js';
4
  import {CloudService} from './services/cloudService.js';
5
  import {Evaluator} from './evaluator.js';
6
+ import {getNumberOfWords, logTo, sleep} from './utils.js';
7
 
8
 
9
  // get references to html elements
 
13
 
14
 
15
  // instantiate services and components
 
16
  const onDeviceInferenceService = new OnDeviceService(getModelSelection());
17
  const cloudInferenceService = new CloudService({
18
  apiKey: document.getElementById('cloudApiKey').value,
 
29
  });
30
 
31
 
32
+ // instantiate the job scheduler with some mock prompts
33
  const scheduler = new JobScheduler('boolq_validation');
34
 
35
 
 
77
 
78
  document.getElementById('stopBtn').addEventListener('click', () => {
79
  scheduler.stop();
80
+ isExperimentRunning = false;
81
  document.getElementById('startBtn').disabled = false;
82
  document.getElementById('stopBtn').disabled = true;
83
  });
84
 
 
 
 
85
  document.getElementById('downloadStatsCsv').addEventListener('click', () => {
86
  downloadStatsAsCSV();
87
  });
 
100
  let experimentJobCount = 0;
101
  let experimentTargetJobs = 0;
102
  let isExperimentRunning = false;
103
+ const TARGET_JOBS = 500;
104
 
105
  document.getElementById('start1000Btn').addEventListener('click', async () => {
106
 
 
131
  cloudModel,
132
  routeStrategy,
133
  pattern,
134
+ startTime: Date.now()
135
  };
136
 
137
  experimentJobCount = 0;
 
180
  });
181
 
182
  function finishExperiment() {
 
 
183
  isExperimentRunning = false;
184
  console.log('βœ… Experiment complete!');
185
 
 
217
  const stats = {
218
  experiment: {
219
  ...currentExperiment,
220
+ endTime: Date.now(),
221
  completedJobs: requestManager.stats.count
222
  },
223
  stats: requestManager.stats
 
252
  const lines = [];
253
 
254
  // Header
255
+ lines.push('dataset_item_id,route,latency_ms,total_latency_ms,queueing_time_ms,inference_time_ms,exact_match,f1_score,ground_truth,answer,job_start_ts,inference_start_ts,inference_end_ts,prompt,number_of_words,number_of_characters,experiment_start_time_ms,experiment_end_time_ms,route_strategy,pattern,device_model,cloud_model');
256
 
257
  // Data rows
258
  stats.stats.results.forEach((result, index) => {
259
  const row = [
260
+ result.job.id,
261
  result.route || '',
262
  (result.latency || 0).toFixed(2),
263
  (result.totalLatency || 0).toFixed(2),
 
266
  result.evalRes?.exactMatch || false,
267
  (result.evalRes?.f1WordLevel || 0).toFixed(4),
268
  `"${(result.job?.groundTruth || '').replace(/"/g, '""')}"`,
269
+ `"${(result.text?.answer || '').replace(/"/g, '""')}"`,
270
+ result.job.timestamps.jobStart || 0,
271
+ result.job.timestamps.inferenceStart || 0,
272
+ result.job.timestamps.inferenceEnd || 0,
273
+ `"${(result.job.prompt || '').replace(/"/g, '""')}"`,
274
+ getNumberOfWords(result.job.prompt || ''),
275
+ result.job.prompt.length,
276
+ stats.experiment.startTime || 0,
277
+ stats.experiment.endTime || 0,
278
+ stats.experiment.routeStrategy,
279
+ stats.experiment.pattern,
280
+ stats.experiment.deviceModel,
281
+ stats.experiment.cloudModel
282
  ];
283
  lines.push(row.join(','));
284
  });
 
410
  }
411
  }
412
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  function downloadStatsAsCSV() {
 
414
 
415
+ // make the stats compatible with buildExperimentCSV method for reuse
416
+ const stats = {
417
+ experiment: {
418
+ deviceModel: getModelSelection().modelName,
419
+ cloudModel: document.getElementById('cloudModel').value,
420
+ routeStrategy: document.getElementById('routeStrategy').value,
421
+ pattern: document.getElementById('patternSelect').value,
422
+ startTime: null,
423
+ endTime: Date.now(),
424
+ completedJobs: requestManager.stats.count
425
+ },
426
+ stats: requestManager.stats
427
+ };
428
+
429
+ const csvContent = buildExperimentCSV(stats);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
 
431
  const dataStr = "data:text/csv;charset=utf-8," + encodeURIComponent(csvContent);
432
  const dlAnchorElem = document.createElement('a');
src/requestManager.js CHANGED
@@ -179,18 +179,18 @@ export class RequestManager {
179
  let response, latencyMs, cleanedResponse; // response is object with .answer and .stats
180
  try {
181
  // Mark inference start
182
- job.timestamps.inferenceStart = performance.now();
183
 
184
  const {res, ms} = await measureAsync(() => service.infer(full_prompt));
185
  response = res;
186
  latencyMs = ms;
187
 
188
  // Mark inference end
189
- job.timestamps.inferenceEnd = performance.now();
190
  } catch (err) {
191
  response = `__error__:${err.message}`;
192
  latencyMs = -1;
193
- job.timestamps.inferenceEnd = performance.now();
194
  }
195
 
196
  // Calculate timing metrics
 
179
  let response, latencyMs, cleanedResponse; // response is object with .answer and .stats
180
  try {
181
  // Mark inference start
182
+ job.timestamps.inferenceStart = Date.now();
183
 
184
  const {res, ms} = await measureAsync(() => service.infer(full_prompt));
185
  response = res;
186
  latencyMs = ms;
187
 
188
  // Mark inference end
189
+ job.timestamps.inferenceEnd = Date.now();
190
  } catch (err) {
191
  response = `__error__:${err.message}`;
192
  latencyMs = -1;
193
+ job.timestamps.inferenceEnd = Date.now();
194
  }
195
 
196
  // Calculate timing metrics
src/scheduler.js CHANGED
@@ -24,38 +24,55 @@ export class JobScheduler {
24
 
25
  /**
26
  * Start emitting jobs based on the selected pattern
27
- * TODO: Implement different patterns to simulate
28
- * TODO: Run different datasets instead of just simple prompts
29
- * @param patternName
30
- * @returns {Promise<void>}
31
  */
32
- async startPattern(patternName) {
33
  this.running = true;
 
 
 
 
 
34
 
35
- // once per second until user stopp evaluation
36
  if (patternName === 'once-per-sec') {
37
- let i = 0;
38
- while (this._dataset.length > 0 && this.running) {
39
- const item = this._dataset.shift(); //shift instead of pop for FIFO
40
  this._emit(item);
41
- await sleep(1000);
 
 
 
42
  }
43
  } else if (patternName === 'every-ten-sec') {
44
- let i = 0;
45
- while (this._dataset.length > 0 && this.running) {
46
  const item = this._dataset.shift();
47
  this._emit(item);
48
- await sleep(10000);
 
 
 
49
  }
50
  } else if (patternName === 'exponential-arrival') {
51
- let i = 0;
52
- while (this._dataset.length > 0 && this.running) {
53
  const item = this._dataset.shift();
54
  this._emit(item);
55
- const timeToNextArrival = this._generateInterarrivalTime(this._interArrivalTimeLambda);
56
- await sleep(timeToNextArrival);
 
 
 
57
  }
58
  }
 
 
 
 
 
 
 
 
59
  }
60
 
61
 
@@ -98,10 +115,11 @@ export class JobScheduler {
98
  _emit(item) {
99
  if (this._onJob) {
100
  const job = {
 
101
  prompt: item.prompt,
102
  groundTruth: item.groundTruth,
103
  timestamps: {
104
- jobStart: performance.now(),
105
  inferenceStart: null,
106
  inferenceEnd: null
107
  }
@@ -141,15 +159,15 @@ export class JobScheduler {
141
  }
142
  fields.push(field);
143
  }
144
- const [question, answer, context] = fields;
145
-
146
  // More explicit prompt to get concise answers
147
  const full_prompt = `Question: ${question}
148
  Context: ${context}
149
  Instructions: Answer with ONLY the word "true" or "false". Do not provide any explanation or additional text.
150
  Answer:`;
151
 
152
- return {prompt: full_prompt, groundTruth: answer};
153
  });
154
  console.log(`βœ… Dataset '${name}' loaded with ${this._dataset.length} items.`);
155
  })
@@ -160,13 +178,13 @@ export class JobScheduler {
160
 
161
 
162
  /**
163
- * Generate interarrival time based on exponential distribution
164
  *
165
  * @param lambda - rate parameter (requests per second)
166
  * @returns {number} - interarrival time in milliseconds
167
  * @private
168
  */
169
- _generateInterarrivalTime(lambda) {
170
  const u = Math.random(); // uniform random number between 0 and 1
171
  return -Math.log(u) / lambda * 1000; // convert to milliseconds
172
  }
 
24
 
25
  /**
26
  * Start emitting jobs based on the selected pattern
27
+ * @param {string} patternName - The pattern to use
28
+ * @param {number} maxJobs - Maximum number of jobs to emit (defaults to Infinity)
29
+ * @returns {Promise<number>} - Number of jobs emitted
 
30
  */
31
+ async startPattern(patternName, maxJobs = Infinity) {
32
  this.running = true;
33
+ let jobsEmitted = 0;
34
+
35
+ if (maxJobs !== Infinity) {
36
+ console.log(`πŸš€ Starting limited run: ${maxJobs} jobs with pattern '${patternName}'`);
37
+ }
38
 
 
39
  if (patternName === 'once-per-sec') {
40
+ while (this._dataset.length > 0 && this.running && jobsEmitted < maxJobs) {
41
+ const item = this._dataset.shift();
 
42
  this._emit(item);
43
+ jobsEmitted++;
44
+ if (jobsEmitted < maxJobs && this._dataset.length > 0 && this.running) {
45
+ await sleep(1000);
46
+ }
47
  }
48
  } else if (patternName === 'every-ten-sec') {
49
+ while (this._dataset.length > 0 && this.running && jobsEmitted < maxJobs) {
 
50
  const item = this._dataset.shift();
51
  this._emit(item);
52
+ jobsEmitted++;
53
+ if (jobsEmitted < maxJobs && this._dataset.length > 0 && this.running) {
54
+ await sleep(10000);
55
+ }
56
  }
57
  } else if (patternName === 'exponential-arrival') {
58
+ while (this._dataset.length > 0 && this.running && jobsEmitted < maxJobs) {
 
59
  const item = this._dataset.shift();
60
  this._emit(item);
61
+ jobsEmitted++;
62
+ if (jobsEmitted < maxJobs && this._dataset.length > 0 && this.running) {
63
+ const timeToNextArrival = this._generateExponentialInterarrivalTime(this._interArrivalTimeLambda);
64
+ await sleep(timeToNextArrival);
65
+ }
66
  }
67
  }
68
+
69
+ if (maxJobs !== Infinity) {
70
+ console.log(`βœ… Limited run complete: ${jobsEmitted} jobs emitted.`);
71
+ } else {
72
+ console.log(`πŸ›‘ Job emission stopped. Total jobs emitted: ${jobsEmitted}`);
73
+ }
74
+
75
+ return jobsEmitted;
76
  }
77
 
78
 
 
115
  _emit(item) {
116
  if (this._onJob) {
117
  const job = {
118
+ id: item.id,
119
  prompt: item.prompt,
120
  groundTruth: item.groundTruth,
121
  timestamps: {
122
+ jobStart: Date.now(),
123
  inferenceStart: null,
124
  inferenceEnd: null
125
  }
 
159
  }
160
  fields.push(field);
161
  }
162
+ const [id, question, answer, context] = fields;
163
+
164
  // More explicit prompt to get concise answers
165
  const full_prompt = `Question: ${question}
166
  Context: ${context}
167
  Instructions: Answer with ONLY the word "true" or "false". Do not provide any explanation or additional text.
168
  Answer:`;
169
 
170
+ return {id: id, prompt: full_prompt, groundTruth: answer};
171
  });
172
  console.log(`βœ… Dataset '${name}' loaded with ${this._dataset.length} items.`);
173
  })
 
178
 
179
 
180
  /**
181
+ * Generate interarrival time based on exponential interarrival distribution (equals a poisson process)
182
  *
183
  * @param lambda - rate parameter (requests per second)
184
  * @returns {number} - interarrival time in milliseconds
185
  * @private
186
  */
187
+ _generateExponentialInterarrivalTime(lambda) {
188
  const u = Math.random(); // uniform random number between 0 and 1
189
  return -Math.log(u) / lambda * 1000; // convert to milliseconds
190
  }
src/utils.js CHANGED
@@ -26,6 +26,7 @@ export function logTo(el, evt) {
26
  if (!el) return;
27
  const row = document.createElement('tr');
28
  row.innerHTML = `
 
29
  <td>${new Date().toLocaleTimeString()}</td>
30
  <td>${evt.route}</td>
31
  <td>${evt.totalLatency?.toFixed(2) || evt.latency?.toFixed(2) || 0}ms</td>
@@ -37,4 +38,15 @@ export function logTo(el, evt) {
37
  `;
38
  el.appendChild(row);
39
  el.scrollTop = el.scrollHeight;
40
- }
 
 
 
 
 
 
 
 
 
 
 
 
26
  if (!el) return;
27
  const row = document.createElement('tr');
28
  row.innerHTML = `
29
+ <td>${evt.job.id}</td>
30
  <td>${new Date().toLocaleTimeString()}</td>
31
  <td>${evt.route}</td>
32
  <td>${evt.totalLatency?.toFixed(2) || evt.latency?.toFixed(2) || 0}ms</td>
 
38
  `;
39
  el.appendChild(row);
40
  el.scrollTop = el.scrollHeight;
41
+ }
42
+
43
+
44
+ /**
45
+ * Approximates the number of words in a given text string
46
+ *
47
+ * @param text - Input text string
48
+ * @returns {number} - Approximate number of words
49
+ */
50
+ export function getNumberOfWords(text) {
51
+ return text.trim().split(/\s+/).length;
52
+ }