ttnn-visualizer 0.36.0__py3-none-any.whl → 0.37.0__py3-none-any.whl

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.
Files changed (25) hide show
  1. ttnn_visualizer/app.py +2 -2
  2. ttnn_visualizer/csv_queries.py +35 -35
  3. ttnn_visualizer/decorators.py +13 -6
  4. ttnn_visualizer/file_uploads.py +89 -23
  5. ttnn_visualizer/{sessions.py → instances.py} +70 -54
  6. ttnn_visualizer/models.py +1 -1
  7. ttnn_visualizer/queries.py +26 -26
  8. ttnn_visualizer/serializers.py +4 -6
  9. ttnn_visualizer/settings.py +9 -2
  10. ttnn_visualizer/sftp_operations.py +7 -3
  11. ttnn_visualizer/static/assets/{allPaths-ChIeDZ5t.js → allPaths-Z03s-OPC.js} +1 -1
  12. ttnn_visualizer/static/assets/{allPathsLoader-C4OHN8TU.js → allPathsLoader-BnryPsjm.js} +2 -2
  13. ttnn_visualizer/static/assets/{index-D8zG3DIo.js → index-BgzPx-DB.js} +198 -198
  14. ttnn_visualizer/static/assets/{index-BnUuxY3c.css → index-je2tF5Bg.css} +1 -1
  15. ttnn_visualizer/static/assets/{splitPathsBySizeLoader-BL6wqcCx.js → splitPathsBySizeLoader-Ru7hJnSI.js} +1 -1
  16. ttnn_visualizer/static/index.html +2 -2
  17. ttnn_visualizer/tests/test_queries.py +28 -28
  18. ttnn_visualizer/views.py +213 -146
  19. {ttnn_visualizer-0.36.0.dist-info → ttnn_visualizer-0.37.0.dist-info}/METADATA +1 -1
  20. {ttnn_visualizer-0.36.0.dist-info → ttnn_visualizer-0.37.0.dist-info}/RECORD +25 -25
  21. {ttnn_visualizer-0.36.0.dist-info → ttnn_visualizer-0.37.0.dist-info}/LICENSE +0 -0
  22. {ttnn_visualizer-0.36.0.dist-info → ttnn_visualizer-0.37.0.dist-info}/LICENSE_understanding.txt +0 -0
  23. {ttnn_visualizer-0.36.0.dist-info → ttnn_visualizer-0.37.0.dist-info}/WHEEL +0 -0
  24. {ttnn_visualizer-0.36.0.dist-info → ttnn_visualizer-0.37.0.dist-info}/entry_points.txt +0 -0
  25. {ttnn_visualizer-0.36.0.dist-info → ttnn_visualizer-0.37.0.dist-info}/top_level.txt +0 -0
ttnn_visualizer/views.py CHANGED
@@ -13,15 +13,15 @@ import shutil
13
13
 
14
14
  import zstd
15
15
  from flask import Blueprint
16
- from flask import request, current_app
16
+ from flask import current_app, session, request
17
17
 
18
18
  from ttnn_visualizer.csv_queries import DeviceLogProfilerQueries, OpsPerformanceQueries, OpsPerformanceReportQueries
19
- from ttnn_visualizer.decorators import with_session
19
+ from ttnn_visualizer.decorators import with_instance
20
20
  from ttnn_visualizer.enums import ConnectionTestStates
21
21
  from ttnn_visualizer.exceptions import DataFormatError
22
22
  from ttnn_visualizer.exceptions import RemoteConnectionException
23
23
  from ttnn_visualizer.file_uploads import (
24
- extract_profiler_name,
24
+ extract_folder_name_from_files,
25
25
  extract_npe_name,
26
26
  save_uploaded_files,
27
27
  validate_files,
@@ -43,8 +43,8 @@ from ttnn_visualizer.serializers import (
43
43
  serialize_operations_buffers,
44
44
  serialize_devices, serialize_buffer,
45
45
  )
46
- from ttnn_visualizer.sessions import (
47
- update_instance,
46
+ from ttnn_visualizer.instances import (
47
+ get_instances, update_instance,
48
48
  )
49
49
  from ttnn_visualizer.sftp_operations import (
50
50
  sync_remote_profiler_folders,
@@ -69,10 +69,10 @@ api = Blueprint("api", __name__, url_prefix="/api")
69
69
 
70
70
 
71
71
  @api.route("/operations", methods=["GET"])
72
- @with_session
72
+ @with_instance
73
73
  @timer
74
- def operation_list(session):
75
- with DatabaseQueries(session) as db:
74
+ def operation_list(instance: Instance):
75
+ with DatabaseQueries(instance) as db:
76
76
  operations = list(db.query_operations())
77
77
  operations.sort(key=lambda o: o.operation_id)
78
78
  operation_arguments = list(db.query_operation_arguments())
@@ -98,10 +98,10 @@ def operation_list(session):
98
98
 
99
99
 
100
100
  @api.route("/operations/<operation_id>", methods=["GET"])
101
- @with_session
101
+ @with_instance
102
102
  @timer
103
- def operation_detail(operation_id, session):
104
- with DatabaseQueries(session) as db:
103
+ def operation_detail(operation_id, instance: Instance):
104
+ with DatabaseQueries(instance) as db:
105
105
 
106
106
  device_id = request.args.get("device_id", None)
107
107
  operations = list(db.query_operations(filters={"operation_id": operation_id}))
@@ -171,17 +171,17 @@ def operation_detail(operation_id, session):
171
171
 
172
172
 
173
173
  @api.route("operation-history", methods=["GET"])
174
- @with_session
174
+ @with_instance
175
175
  @timer
176
- def operation_history(session: Instance):
176
+ def operation_history(instance: Instance):
177
177
  operation_history_filename = "operation_history.json"
178
- if session.remote_connection and session.remote_connection.useRemoteQuerying:
179
- if not session.remote_folder:
178
+ if instance.remote_connection and instance.remote_connection.useRemoteQuerying:
179
+ if not instance.remote_folder:
180
180
  return []
181
181
  operation_history = read_remote_file(
182
- remote_connection=session.remote_connection,
182
+ remote_connection=instance.remote_connection,
183
183
  remote_path=Path(
184
- session.remote_folder.remotePath, operation_history_filename
184
+ instance.remote_folder.remotePath, operation_history_filename
185
185
  ),
186
186
  )
187
187
  if not operation_history:
@@ -189,7 +189,7 @@ def operation_history(session: Instance):
189
189
  return json.loads(operation_history)
190
190
  else:
191
191
  operation_history_file = (
192
- Path(str(session.profiler_path)).parent / operation_history_filename
192
+ Path(str(instance.profiler_path)).parent / operation_history_filename
193
193
  )
194
194
  if not operation_history_file.exists():
195
195
  return []
@@ -198,21 +198,21 @@ def operation_history(session: Instance):
198
198
 
199
199
 
200
200
  @api.route("/config")
201
- @with_session
201
+ @with_instance
202
202
  @timer
203
- def get_config(session: Instance):
204
- if session.remote_connection and session.remote_connection.useRemoteQuerying:
205
- if not session.remote_profiler_folder:
203
+ def get_config(instance: Instance):
204
+ if instance.remote_connection and instance.remote_connection.useRemoteQuerying:
205
+ if not instance.remote_profiler_folder:
206
206
  return {}
207
207
  config = read_remote_file(
208
- remote_connection=session.remote_connection,
209
- remote_path=Path(session.remote_profiler_folder.remotePath, "config.json"),
208
+ remote_connection=instance.remote_connection,
209
+ remote_path=Path(instance.remote_profiler_folder.remotePath, "config.json"),
210
210
  )
211
211
  if not config:
212
212
  return {}
213
213
  return config
214
214
  else:
215
- config_file = Path(str(session.profiler_path)).parent.joinpath("config.json")
215
+ config_file = Path(str(instance.profiler_path)).parent.joinpath("config.json")
216
216
  if not config_file.exists():
217
217
  return {}
218
218
  with open(config_file, "r") as file:
@@ -220,10 +220,10 @@ def get_config(session: Instance):
220
220
 
221
221
 
222
222
  @api.route("/tensors", methods=["GET"])
223
- @with_session
223
+ @with_instance
224
224
  @timer
225
- def tensors_list(session: Instance):
226
- with DatabaseQueries(session) as db:
225
+ def tensors_list(instance: Instance):
226
+ with DatabaseQueries(instance) as db:
227
227
  device_id = request.args.get("device_id", None)
228
228
  tensors = list(db.query_tensors(filters={"device_id": device_id}))
229
229
  local_comparisons = list(db.query_tensor_comparisons())
@@ -235,9 +235,9 @@ def tensors_list(session: Instance):
235
235
 
236
236
 
237
237
  @api.route("/buffer", methods=["GET"])
238
- @with_session
238
+ @with_instance
239
239
  @timer
240
- def buffer_detail(session: Instance):
240
+ def buffer_detail(instance: Instance):
241
241
  address = request.args.get("address")
242
242
  operation_id = request.args.get("operation_id")
243
243
 
@@ -249,7 +249,7 @@ def buffer_detail(session: Instance):
249
249
  else:
250
250
  return Response(status=HTTPStatus.BAD_REQUEST)
251
251
 
252
- with DatabaseQueries(session) as db:
252
+ with DatabaseQueries(instance) as db:
253
253
  buffer = db.query_next_buffer(operation_id, address)
254
254
  if not buffer:
255
255
  return Response(status=HTTPStatus.NOT_FOUND)
@@ -257,9 +257,9 @@ def buffer_detail(session: Instance):
257
257
 
258
258
 
259
259
  @api.route("/buffer-pages", methods=["GET"])
260
- @with_session
260
+ @with_instance
261
261
  @timer
262
- def buffer_pages(session: Instance):
262
+ def buffer_pages(instance: Instance):
263
263
  address = request.args.get("address")
264
264
  operation_id = request.args.get("operation_id")
265
265
  buffer_type = request.args.get("buffer_type", "")
@@ -275,7 +275,7 @@ def buffer_pages(session: Instance):
275
275
  else:
276
276
  buffer_type = None
277
277
 
278
- with DatabaseQueries(session) as db:
278
+ with DatabaseQueries(instance) as db:
279
279
  buffers = list(
280
280
  list(
281
281
  db.query_buffer_pages(
@@ -292,10 +292,10 @@ def buffer_pages(session: Instance):
292
292
 
293
293
 
294
294
  @api.route("/tensors/<tensor_id>", methods=["GET"])
295
- @with_session
295
+ @with_instance
296
296
  @timer
297
- def tensor_detail(tensor_id, session: Instance):
298
- with DatabaseQueries(session) as db:
297
+ def tensor_detail(tensor_id, instance: Instance):
298
+ with DatabaseQueries(instance) as db:
299
299
  tensors = list(db.query_tensors(filters={"tensor_id": tensor_id}))
300
300
  if not tensors:
301
301
  return Response(status=HTTPStatus.NOT_FOUND)
@@ -304,8 +304,8 @@ def tensor_detail(tensor_id, session: Instance):
304
304
 
305
305
 
306
306
  @api.route("/buffers", methods=["GET"])
307
- @with_session
308
- def get_all_buffers(session: Instance):
307
+ @with_instance
308
+ def get_all_buffers(instance: Instance):
309
309
  buffer_type = request.args.get("buffer_type", "")
310
310
  device_id = request.args.get("device_id", None)
311
311
  if buffer_type and str.isdigit(buffer_type):
@@ -313,7 +313,7 @@ def get_all_buffers(session: Instance):
313
313
  else:
314
314
  buffer_type = None
315
315
 
316
- with DatabaseQueries(session) as db:
316
+ with DatabaseQueries(instance) as db:
317
317
  buffers = list(
318
318
  db.query_buffers(
319
319
  filters={"buffer_type": buffer_type, "device_id": device_id}
@@ -324,8 +324,8 @@ def get_all_buffers(session: Instance):
324
324
 
325
325
 
326
326
  @api.route("/operation-buffers", methods=["GET"])
327
- @with_session
328
- def get_operations_buffers(session: Instance):
327
+ @with_instance
328
+ def get_operations_buffers(instance: Instance):
329
329
  buffer_type = request.args.get("buffer_type", "")
330
330
  device_id = request.args.get("device_id", None)
331
331
  if buffer_type and str.isdigit(buffer_type):
@@ -333,7 +333,7 @@ def get_operations_buffers(session: Instance):
333
333
  else:
334
334
  buffer_type = None
335
335
 
336
- with DatabaseQueries(session) as db:
336
+ with DatabaseQueries(instance) as db:
337
337
  buffers = list(
338
338
  db.query_buffers(
339
339
  filters={"buffer_type": buffer_type, "device_id": device_id}
@@ -344,8 +344,8 @@ def get_operations_buffers(session: Instance):
344
344
 
345
345
 
346
346
  @api.route("/operation-buffers/<operation_id>", methods=["GET"])
347
- @with_session
348
- def get_operation_buffers(operation_id, session: Instance):
347
+ @with_instance
348
+ def get_operation_buffers(operation_id, instance: Instance):
349
349
  buffer_type = request.args.get("buffer_type", "")
350
350
  device_id = request.args.get("device_id", None)
351
351
  if buffer_type and str.isdigit(buffer_type):
@@ -353,7 +353,7 @@ def get_operation_buffers(operation_id, session: Instance):
353
353
  else:
354
354
  buffer_type = None
355
355
 
356
- with DatabaseQueries(session) as db:
356
+ with DatabaseQueries(instance) as db:
357
357
  operations = list(db.query_operations(filters={"operation_id": operation_id}))
358
358
  if not operations:
359
359
  return Response(status=HTTPStatus.NOT_FOUND)
@@ -373,16 +373,16 @@ def get_operation_buffers(operation_id, session: Instance):
373
373
 
374
374
 
375
375
  @api.route("/profiler", methods=["GET"])
376
- @with_session
377
- def get_profiler_data_list(session: Instance):
376
+ @with_instance
377
+ def get_profiler_data_list(instance: Instance):
378
378
  # Doesn't handle remote at the moment
379
- # is_remote = True if session.remote_connection else False
379
+ # is_remote = True if instance.remote_connection else False
380
380
  # config_key = "REMOTE_DATA_DIRECTORY" if is_remote else "LOCAL_DATA_DIRECTORY"
381
381
  config_key = 'LOCAL_DATA_DIRECTORY'
382
382
  data_directory = Path(current_app.config[config_key])
383
383
 
384
384
  # if is_remote:
385
- # connection = RemoteConnection.model_validate(session.remote_connection, strict=False)
385
+ # connection = RemoteConnection.model_validate(instance.remote_connection, strict=False)
386
386
  # path = data_directory / connection.host / current_app.config["PROFILER_DIRECTORY_NAME"]
387
387
  # else:
388
388
  path = data_directory / current_app.config["PROFILER_DIRECTORY_NAME"]
@@ -390,13 +390,38 @@ def get_profiler_data_list(session: Instance):
390
390
  if not path.exists():
391
391
  path.mkdir(parents=True, exist_ok=True)
392
392
 
393
- directory_names = [directory.name for directory in path.iterdir() if directory.is_dir()]
394
-
395
393
  valid_dirs = []
396
394
 
395
+ if current_app.config["SERVER_MODE"]:
396
+ session_instances = session.get("instances", [])
397
+ instances = get_instances(session_instances)
398
+ db_paths = [instance.profiler_path for instance in instances if instance.profiler_path]
399
+ directory_names = [str(Path(db_path).parent.name) for db_path in db_paths]
400
+ else:
401
+ directory_names = [directory.name for directory in path.iterdir() if directory.is_dir()]
402
+
403
+ # Sort directory names by modified time (most recent first)
404
+ def get_modified_time(dir_name):
405
+ dir_path = Path(path) / dir_name
406
+ if dir_path.exists():
407
+ return dir_path.stat().st_mtime
408
+ return 0
409
+
410
+ directory_names.sort(key=get_modified_time, reverse=True)
411
+
397
412
  for dir_name in directory_names:
398
413
  dir_path = Path(path) / dir_name
399
414
  files = list(dir_path.glob("**/*"))
415
+ report_name = None
416
+ config_file = dir_path / "config.json"
417
+
418
+ if config_file.exists():
419
+ try:
420
+ with open(config_file, "r") as f:
421
+ config_data = json.load(f)
422
+ report_name = config_data.get("report_name")
423
+ except Exception as e:
424
+ logger.warning(f"Failed to read config.json in {dir_path}: {e}")
400
425
 
401
426
  # Would like to use the existing validate_files function but there's a type difference I'm not sure how to handle
402
427
  if not any(file.name == "db.sqlite" for file in files):
@@ -404,15 +429,15 @@ def get_profiler_data_list(session: Instance):
404
429
  if not any(file.name == "config.json" for file in files):
405
430
  continue
406
431
 
407
- valid_dirs.append(dir_name)
432
+ valid_dirs.append({"path": dir_path.name, "reportName": report_name})
408
433
 
409
434
  return jsonify(valid_dirs)
410
435
 
411
436
 
412
437
  @api.route("/profiler/<profiler_name>", methods=["DELETE"])
413
- @with_session
414
- def delete_profiler_report(profiler_name, session: Instance):
415
- is_remote = bool(session.remote_connection)
438
+ @with_instance
439
+ def delete_profiler_report(profiler_name, instance: Instance):
440
+ is_remote = bool(instance.remote_connection)
416
441
  config_key = "REMOTE_DATA_DIRECTORY" if is_remote else "LOCAL_DATA_DIRECTORY"
417
442
  data_directory = Path(current_app.config[config_key])
418
443
 
@@ -420,12 +445,12 @@ def delete_profiler_report(profiler_name, session: Instance):
420
445
  return Response(status=HTTPStatus.BAD_REQUEST, response="Report name is required.")
421
446
 
422
447
  if is_remote:
423
- connection = RemoteConnection.model_validate(session.remote_connection, strict=False)
448
+ connection = RemoteConnection.model_validate(instance.remote_connection, strict=False)
424
449
  path = data_directory / connection.host / current_app.config["PROFILER_DIRECTORY_NAME"]
425
450
  else:
426
451
  path = data_directory / current_app.config["PROFILER_DIRECTORY_NAME"] / profiler_name
427
452
 
428
- if session.active_report and session.active_report.profiler_name == profiler_name:
453
+ if instance.active_report and instance.active_report.profiler_name == profiler_name:
429
454
  instance_id = request.args.get("instanceId")
430
455
  update_instance(instance_id=instance_id,profiler_name="")
431
456
 
@@ -439,26 +464,38 @@ def delete_profiler_report(profiler_name, session: Instance):
439
464
 
440
465
 
441
466
  @api.route("/performance", methods=["GET"])
442
- @with_session
443
- def get_performance_data_list(session: Instance):
444
- is_remote = True if session.remote_connection else False
467
+ @with_instance
468
+ def get_performance_data_list(instance: Instance):
469
+ is_remote = True if instance.remote_connection else False
445
470
  config_key = "REMOTE_DATA_DIRECTORY" if is_remote else "LOCAL_DATA_DIRECTORY"
446
- config_key = 'LOCAL_DATA_DIRECTORY'
447
471
  data_directory = Path(current_app.config[config_key])
472
+ path = data_directory / current_app.config["PERFORMANCE_DIRECTORY_NAME"]
448
473
 
449
- if is_remote:
450
- connection = RemoteConnection.model_validate(session.remote_connection, strict=False)
451
- path = data_directory / connection.host / current_app.config["PERFORMANCE_DIRECTORY_NAME"]
452
- else:
453
- path = data_directory / current_app.config["PERFORMANCE_DIRECTORY_NAME"]
454
-
455
- if not path.exists():
474
+ if not is_remote and not path.exists():
456
475
  path.mkdir(parents=True, exist_ok=True)
457
476
 
458
- directory_names = [directory.name for directory in path.iterdir() if directory.is_dir()]
477
+ if current_app.config["SERVER_MODE"]:
478
+ session_instances = session.get("instances", [])
479
+ instances = get_instances(session_instances)
480
+ db_paths = [instance.performance_path for instance in instances if instance.performance_path]
481
+ directory_names = [str(Path(db_path).name) for db_path in db_paths]
482
+ else:
483
+ if is_remote:
484
+ connection = RemoteConnection.model_validate(instance.remote_connection, strict=False)
485
+ path = data_directory / connection.host / current_app.config["PERFORMANCE_DIRECTORY_NAME"]
486
+ directory_names = [directory.name for directory in path.iterdir() if directory.is_dir()]
459
487
 
460
488
  valid_dirs = []
461
489
 
490
+ # Sort directory names by modified time (most recent first)
491
+ def get_modified_time(dir_name):
492
+ dir_path = Path(path) / dir_name
493
+ if dir_path.exists():
494
+ return dir_path.stat().st_mtime
495
+ return 0
496
+
497
+ directory_names.sort(key=get_modified_time, reverse=True)
498
+
462
499
  for dir_name in directory_names:
463
500
  dir_path = Path(path) / dir_name
464
501
  files = list(dir_path.glob("**/*"))
@@ -471,36 +508,36 @@ def get_performance_data_list(session: Instance):
471
508
  if not any(file.name.startswith("ops_perf_results") for file in files):
472
509
  continue
473
510
 
474
- valid_dirs.append(dir_name)
511
+ valid_dirs.append({"path": dir_path.name, "reportName": dir_path.name})
475
512
 
476
513
  return jsonify(valid_dirs)
477
514
 
478
515
 
479
516
  @api.route("/performance/device-log", methods=["GET"])
480
- @with_session
481
- def get_performance_data(session: Instance):
482
- if not session.performance_path:
517
+ @with_instance
518
+ def get_performance_data(instance: Instance):
519
+ if not instance.performance_path:
483
520
  return Response(status=HTTPStatus.NOT_FOUND)
484
- with DeviceLogProfilerQueries(session) as csv:
521
+ with DeviceLogProfilerQueries(instance) as csv:
485
522
  result = csv.get_all_entries(as_dict=True, limit=100)
486
523
  return jsonify(result)
487
524
 
488
525
 
489
526
  @api.route("/performance/perf-results", methods=["GET"])
490
- @with_session
491
- def get_profiler_performance_data(session: Instance):
492
- if not session.performance_path:
527
+ @with_instance
528
+ def get_profiler_performance_data(instance: Instance):
529
+ if not instance.performance_path:
493
530
  return Response(status=HTTPStatus.NOT_FOUND)
494
- with OpsPerformanceQueries(session) as csv:
531
+ with OpsPerformanceQueries(instance) as csv:
495
532
  # result = csv.query_by_op_code(op_code="(torch) contiguous", as_dict=True)
496
533
  result = csv.get_all_entries(as_dict=True, limit=100)
497
534
  return jsonify(result)
498
535
 
499
536
 
500
537
  @api.route("/performance/<performance_name>", methods=["DELETE"])
501
- @with_session
502
- def delete_performance_report(performance_name, session: Instance):
503
- is_remote = bool(session.remote_connection)
538
+ @with_instance
539
+ def delete_performance_report(performance_name, instance: Instance):
540
+ is_remote = bool(instance.remote_connection)
504
541
  config_key = "REMOTE_DATA_DIRECTORY" if is_remote else "LOCAL_DATA_DIRECTORY"
505
542
  data_directory = Path(current_app.config[config_key])
506
543
 
@@ -508,12 +545,12 @@ def delete_performance_report(performance_name, session: Instance):
508
545
  return Response(status=HTTPStatus.BAD_REQUEST, response="Report name is required.")
509
546
 
510
547
  if is_remote:
511
- connection = RemoteConnection.model_validate(session.remote_connection, strict=False)
548
+ connection = RemoteConnection.model_validate(instance.remote_connection, strict=False)
512
549
  path = data_directory / connection.host / current_app.config["PERFORMANCE_DIRECTORY_NAME"]
513
550
  else:
514
551
  path = data_directory / current_app.config["PERFORMANCE_DIRECTORY_NAME"] / performance_name
515
552
 
516
- if session.active_report and session.active_report.performance_name == performance_name:
553
+ if instance.active_report and instance.active_report.performance_name == performance_name:
517
554
  instance_id = request.args.get("instanceId")
518
555
  update_instance(instance_id=instance_id,performance_name="")
519
556
 
@@ -526,11 +563,11 @@ def delete_performance_report(performance_name, session: Instance):
526
563
 
527
564
 
528
565
  @api.route("/performance/perf-results/raw", methods=["GET"])
529
- @with_session
530
- def get_performance_results_data_raw(session: Instance):
531
- if not session.performance_path:
566
+ @with_instance
567
+ def get_performance_results_data_raw(instance: Instance):
568
+ if not instance.performance_path:
532
569
  return Response(status=HTTPStatus.NOT_FOUND)
533
- content = OpsPerformanceQueries.get_raw_csv(session)
570
+ content = OpsPerformanceQueries.get_raw_csv(instance)
534
571
  return Response(
535
572
  content,
536
573
  mimetype="text/csv",
@@ -539,20 +576,20 @@ def get_performance_results_data_raw(session: Instance):
539
576
 
540
577
 
541
578
  @api.route("/performance/perf-results/report", methods=["GET"])
542
- @with_session
543
- def get_performance_results_report(session: Instance):
544
- if not session.performance_path:
579
+ @with_instance
580
+ def get_performance_results_report(instance: Instance):
581
+ if not instance.performance_path:
545
582
  return Response(status=HTTPStatus.NOT_FOUND)
546
583
 
547
584
  name = request.args.get("name", None)
548
- performance_path = Path(session.performance_path)
585
+ performance_path = Path(instance.performance_path)
549
586
  if name:
550
587
  performance_path = performance_path.parent / name
551
- session.performance_path = str(performance_path)
552
- logger.info(f"************ Profiler path set to {session.performance_path}")
588
+ instance.performance_path = str(performance_path)
589
+ logger.info(f"************ Performance path set to {instance.performance_path}")
553
590
 
554
591
  try:
555
- report = OpsPerformanceReportQueries.generate_report(session)
592
+ report = OpsPerformanceReportQueries.generate_report(instance)
556
593
  except DataFormatError:
557
594
  return Response(status=HTTPStatus.UNPROCESSABLE_ENTITY)
558
595
 
@@ -560,11 +597,11 @@ def get_performance_results_report(session: Instance):
560
597
 
561
598
 
562
599
  @api.route("/performance/device-log/raw", methods=["GET"])
563
- @with_session
564
- def get_performance_data_raw(session: Instance):
565
- if not session.performance_path:
600
+ @with_instance
601
+ def get_performance_data_raw(instance: Instance):
602
+ if not instance.performance_path:
566
603
  return Response(status=HTTPStatus.NOT_FOUND)
567
- content = DeviceLogProfilerQueries.get_raw_csv(session)
604
+ content = DeviceLogProfilerQueries.get_raw_csv(instance)
568
605
  return Response(
569
606
  content,
570
607
  mimetype="text/csv",
@@ -573,19 +610,19 @@ def get_performance_data_raw(session: Instance):
573
610
 
574
611
 
575
612
  @api.route("/performance/device-log/zone/<zone>", methods=["GET"])
576
- @with_session
577
- def get_zone_statistics(zone, session: Instance):
578
- if not session.performance_path:
613
+ @with_instance
614
+ def get_zone_statistics(zone, instance: Instance):
615
+ if not instance.performance_path:
579
616
  return Response(status=HTTPStatus.NOT_FOUND)
580
- with DeviceLogProfilerQueries(session) as csv:
617
+ with DeviceLogProfilerQueries(instance) as csv:
581
618
  result = csv.query_zone_statistics(zone_name=zone, as_dict=True)
582
619
  return jsonify(result)
583
620
 
584
621
 
585
622
  @api.route("/devices", methods=["GET"])
586
- @with_session
587
- def get_devices(session: Instance):
588
- with DatabaseQueries(session) as db:
623
+ @with_instance
624
+ def get_devices(instance: Instance):
625
+ with DatabaseQueries(instance) as db:
589
626
  devices = list(db.query_devices())
590
627
  return serialize_devices(devices)
591
628
 
@@ -593,7 +630,7 @@ def get_devices(session: Instance):
593
630
  @api.route("/local/upload/profiler", methods=["POST"])
594
631
  def create_profiler_files():
595
632
  files = request.files.getlist("files")
596
- folder_name = request.form.get("folderName") # Optional folder name
633
+ folder_name = request.form.get("folderName") # Optional folder name - Used for Safari compatibility
597
634
  profiler_directory = current_app.config["LOCAL_DATA_DIRECTORY"] / current_app.config["PROFILER_DIRECTORY_NAME"]
598
635
 
599
636
  if not validate_files(files, {"db.sqlite", "config.json"}, folder_name=folder_name):
@@ -606,21 +643,35 @@ def create_profiler_files():
606
643
  profiler_directory.mkdir(parents=True, exist_ok=True)
607
644
 
608
645
  if folder_name:
609
- profiler_name = folder_name
646
+ parent_folder_name = folder_name
610
647
  else:
611
- profiler_name = extract_profiler_name(files)
648
+ parent_folder_name = extract_folder_name_from_files(files)
612
649
 
613
- logger.info(f"Writing report files to {profiler_directory}/{profiler_name}")
650
+ logger.info(f"Writing report files to {profiler_directory}/{parent_folder_name}")
614
651
 
615
- save_uploaded_files(files, profiler_directory, folder_name)
652
+ try:
653
+ save_uploaded_files(files, profiler_directory, folder_name)
654
+ except DataFormatError:
655
+ return Response(status=HTTPStatus.UNPROCESSABLE_ENTITY)
616
656
 
617
657
  instance_id = request.args.get("instanceId")
618
- update_instance(instance_id=instance_id, profiler_name=profiler_name, clear_remote=True)
658
+ update_instance(instance_id=instance_id, profiler_name=parent_folder_name, clear_remote=True)
619
659
 
620
- return StatusMessage(
621
- status=ConnectionTestStates.OK, message="Success."
622
- ).model_dump()
660
+ config_file = profiler_directory / parent_folder_name / "config.json"
661
+ report_name = None
623
662
 
663
+ if config_file.exists():
664
+ try:
665
+ with open(config_file, "r") as f:
666
+ config_data = json.load(f)
667
+ report_name = config_data.get("report_name")
668
+ except Exception as e:
669
+ logger.warning(f"Failed to read config.json in {config_file}: {e}")
670
+
671
+ return {
672
+ "path": parent_folder_name,
673
+ "reportName": report_name,
674
+ }
624
675
 
625
676
  @api.route("/local/upload/performance", methods=["POST"])
626
677
  def create_profile_files():
@@ -645,21 +696,24 @@ def create_profile_files():
645
696
  target_directory.mkdir(parents=True, exist_ok=True)
646
697
 
647
698
  if folder_name:
648
- performance_name = folder_name
699
+ parent_folder_name = folder_name
649
700
  else:
650
- performance_name = extract_profiler_name(files)
701
+ parent_folder_name = extract_folder_name_from_files(files)
651
702
 
652
- logger.info(f"Writing performance files to {target_directory}/{performance_name}")
703
+ logger.info(f"Writing performance files to {target_directory}/{parent_folder_name}")
653
704
 
654
- save_uploaded_files(
655
- files,
656
- target_directory,
657
- folder_name
658
- )
705
+ try:
706
+ save_uploaded_files(
707
+ files,
708
+ target_directory,
709
+ folder_name
710
+ )
711
+ except DataFormatError:
712
+ return Response(status=HTTPStatus.UNPROCESSABLE_ENTITY)
659
713
 
660
714
  instance_id = request.args.get("instanceId")
661
715
  update_instance(
662
- instance_id=instance_id, performance_name=performance_name, clear_remote=True
716
+ instance_id=instance_id, performance_name=parent_folder_name, clear_remote=True
663
717
  )
664
718
 
665
719
  return StatusMessage(
@@ -683,7 +737,10 @@ def create_npe_files():
683
737
  target_directory = data_directory / current_app.config["NPE_DIRECTORY_NAME"]
684
738
  target_directory.mkdir(parents=True, exist_ok=True)
685
739
 
686
- save_uploaded_files(files, target_directory)
740
+ try:
741
+ save_uploaded_files(files, target_directory)
742
+ except DataFormatError:
743
+ return Response(status=HTTPStatus.UNPROCESSABLE_ENTITY)
687
744
 
688
745
  instance_id = request.args.get("instanceId")
689
746
  update_instance(instance_id=instance_id, npe_name=npe_name, clear_remote=True)
@@ -746,11 +803,11 @@ import yaml
746
803
 
747
804
 
748
805
  @api.route("/cluster-descriptor", methods=["GET"])
749
- @with_session
750
- def get_cluster_descriptor(session: Instance):
751
- if session.remote_connection:
806
+ @with_instance
807
+ def get_cluster_descriptor(instance: Instance):
808
+ if instance.remote_connection:
752
809
  try:
753
- cluster_desc_file = get_cluster_desc(session.remote_connection)
810
+ cluster_desc_file = get_cluster_desc(instance.remote_connection)
754
811
  if not cluster_desc_file:
755
812
  return jsonify({"error": "cluster_descriptor.yaml not found"}), 404
756
813
  yaml_data = yaml.safe_load(cluster_desc_file.decode("utf-8"))
@@ -765,7 +822,7 @@ def get_cluster_descriptor(session: Instance):
765
822
  except Exception as e:
766
823
  return jsonify({"error": f"An unexpected error occurred: {str(e)}"}), 500
767
824
  else:
768
- local_path = get_cluster_descriptor_path(session)
825
+ local_path = get_cluster_descriptor_path(instance)
769
826
 
770
827
  if not local_path:
771
828
  return jsonify({"error": "cluster_descriptor.yaml not found"}), 404
@@ -942,11 +999,12 @@ def use_remote_folder():
942
999
  remote_performance_folder = None
943
1000
  if profile:
944
1001
  remote_performance_folder = RemoteReportFolder.model_validate(profile, strict=False)
945
- performance_name = remote_performance_folder.testName
1002
+ performance_name = remote_performance_folder.reportName
946
1003
  data_directory = current_app.config["REMOTE_DATA_DIRECTORY"]
947
- profiler_name = Path(folder.remotePath).name
1004
+ profiler_name = folder.reportName
1005
+ folder_name = folder.remotePath.split("/")[-1]
948
1006
 
949
- connection_directory = Path(data_directory, connection.host, current_app.config["PROFILER_DIRECTORY_NAME"], profiler_name)
1007
+ connection_directory = Path(data_directory, connection.host, current_app.config["PROFILER_DIRECTORY_NAME"], folder_name)
950
1008
 
951
1009
  if not connection.useRemoteQuerying and not connection_directory.exists():
952
1010
  return Response(
@@ -976,14 +1034,14 @@ def health_check():
976
1034
  return Response(status=HTTPStatus.OK)
977
1035
 
978
1036
 
979
- @api.route("/session", methods=["GET"])
980
- @with_session
981
- def get_instance(session: Instance):
1037
+ @api.route("/instance", methods=["GET"])
1038
+ @with_instance
1039
+ def get_instance(instance: Instance):
982
1040
  # Used to gate UI functions if no report is active
983
- return session.model_dump()
1041
+ return instance.model_dump()
984
1042
 
985
1043
 
986
- @api.route("/session", methods=["PUT"])
1044
+ @api.route("/instance", methods=["PUT"])
987
1045
  def update_current_instance():
988
1046
  try:
989
1047
  update_data = request.get_json()
@@ -1004,24 +1062,24 @@ def update_current_instance():
1004
1062
 
1005
1063
  return Response(status=HTTPStatus.OK)
1006
1064
  except Exception as e:
1007
- logger.error(f"Error updating session: {str(e)}")
1065
+ logger.error(f"Error updating instance: {str(e)}")
1008
1066
 
1009
1067
  return Response(
1010
1068
  status=HTTPStatus.INTERNAL_SERVER_ERROR,
1011
- response="An error occurred while updating the session.",
1069
+ response="An error occurred while updating the instance.",
1012
1070
  )
1013
1071
 
1014
1072
 
1015
1073
  @api.route("/npe", methods=["GET"])
1016
- @with_session
1074
+ @with_instance
1017
1075
  @timer
1018
- def get_npe_data(session: Instance):
1019
- if not session.npe_path:
1020
- logger.error("NPE path is not set in the session.")
1076
+ def get_npe_data(instance: Instance):
1077
+ if not instance.npe_path:
1078
+ logger.error("NPE path is not set in the instance.")
1021
1079
  return Response(status=HTTPStatus.NOT_FOUND)
1022
1080
 
1023
- compressed_path = Path(f"{session.npe_path}/{session.active_report.npe_name}.npeviz.zst")
1024
- uncompressed_path = Path(f"{session.npe_path}/{session.active_report.npe_name}.json")
1081
+ compressed_path = Path(f"{instance.npe_path}/{instance.active_report.npe_name}.npeviz.zst")
1082
+ uncompressed_path = Path(f"{instance.npe_path}/{instance.active_report.npe_name}.json")
1025
1083
 
1026
1084
  if not compressed_path.exists() and not uncompressed_path.exists():
1027
1085
  logger.error(f"NPE file does not exist: {compressed_path} / {uncompressed_path}")
@@ -1037,3 +1095,12 @@ def get_npe_data(session: Instance):
1037
1095
  npe_data = json.load(file)
1038
1096
 
1039
1097
  return jsonify(npe_data)
1098
+
1099
+
1100
+ @api.route("/config.js", methods=["GET"])
1101
+ def config_js():
1102
+ config = {
1103
+ "SERVER_MODE": current_app.config["SERVER_MODE"],
1104
+ }
1105
+ js = f"window.TTNN_VISUALIZER_CONFIG = {json.dumps(config)};"
1106
+ return Response(js, mimetype="application/javascript")