ttnn-visualizer 0.49.0__py3-none-any.whl → 0.64.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 (36) hide show
  1. ttnn_visualizer/app.py +151 -49
  2. ttnn_visualizer/csv_queries.py +154 -45
  3. ttnn_visualizer/decorators.py +0 -9
  4. ttnn_visualizer/exceptions.py +0 -7
  5. ttnn_visualizer/models.py +20 -1
  6. ttnn_visualizer/queries.py +8 -0
  7. ttnn_visualizer/serializers.py +53 -9
  8. ttnn_visualizer/settings.py +24 -10
  9. ttnn_visualizer/ssh_client.py +1 -4
  10. ttnn_visualizer/static/assets/allPaths-DWjqav_8.js +1 -0
  11. ttnn_visualizer/static/assets/allPathsLoader-B0eRT9aL.js +2 -0
  12. ttnn_visualizer/static/assets/index-BE2R-cuu.css +1 -0
  13. ttnn_visualizer/static/assets/index-BZITDwoa.js +1 -0
  14. ttnn_visualizer/static/assets/{index-DVrPLQJ7.js → index-DDrUX09k.js} +274 -479
  15. ttnn_visualizer/static/assets/index-voJy5fZe.js +1 -0
  16. ttnn_visualizer/static/assets/splitPathsBySizeLoader-_GpmIkFm.js +1 -0
  17. ttnn_visualizer/static/index.html +2 -2
  18. ttnn_visualizer/tests/test_serializers.py +2 -0
  19. ttnn_visualizer/tests/test_utils.py +362 -0
  20. ttnn_visualizer/utils.py +142 -0
  21. ttnn_visualizer/views.py +181 -87
  22. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/METADATA +58 -30
  23. ttnn_visualizer-0.64.0.dist-info/RECORD +44 -0
  24. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/licenses/LICENSE +6 -0
  25. ttnn_visualizer/remote_sqlite_setup.py +0 -100
  26. ttnn_visualizer/static/assets/allPaths-G_CNx_x1.js +0 -1
  27. ttnn_visualizer/static/assets/allPathsLoader-s_Yfmxfp.js +0 -2
  28. ttnn_visualizer/static/assets/index-CnPrfHYh.js +0 -1
  29. ttnn_visualizer/static/assets/index-Cnc1EkDo.js +0 -1
  30. ttnn_visualizer/static/assets/index-UuXdrHif.css +0 -7
  31. ttnn_visualizer/static/assets/splitPathsBySizeLoader-ivxxaHxa.js +0 -1
  32. ttnn_visualizer-0.49.0.dist-info/RECORD +0 -44
  33. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/WHEEL +0 -0
  34. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/entry_points.txt +0 -0
  35. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/licenses/LICENSE_understanding.txt +0 -0
  36. {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/top_level.txt +0 -0
ttnn_visualizer/app.py CHANGED
@@ -25,7 +25,7 @@ from ttnn_visualizer.exceptions import (
25
25
  )
26
26
  from ttnn_visualizer.instances import create_instance_from_local_paths
27
27
  from ttnn_visualizer.settings import Config, DefaultConfig
28
- from ttnn_visualizer.utils import create_path_resolver
28
+ from ttnn_visualizer.utils import find_gunicorn_path, get_app_data_directory
29
29
  from werkzeug.debug import DebuggedApplication
30
30
  from werkzeug.middleware.proxy_fix import ProxyFix
31
31
 
@@ -55,8 +55,9 @@ def create_app(settings_override=None):
55
55
  static_folder=config.STATIC_ASSETS_DIR,
56
56
  static_url_path=f"{config.BASE_PATH}static",
57
57
  )
58
- logging.basicConfig(level=app.config.get("LOG_LEVEL", "INFO"))
59
58
 
59
+ # logging.basicConfig(level=app.config.get("LOG_LEVEL", "DEBUG"))
60
+ logging.basicConfig(level=logging.DEBUG)
60
61
  app.config.from_object(config)
61
62
 
62
63
  if settings_override:
@@ -80,6 +81,7 @@ def create_app(settings_override=None):
80
81
  "SERVER_MODE": app.config["SERVER_MODE"],
81
82
  "BASE_PATH": app.config["BASE_PATH"],
82
83
  "TT_METAL_HOME": app.config["TT_METAL_HOME"],
84
+ "REPORT_DATA_DIRECTORY": str(app.config["REPORT_DATA_DIRECTORY"]),
83
85
  }
84
86
  js = f"window.TTNN_VISUALIZER_CONFIG = {json.dumps(js_config)};"
85
87
 
@@ -114,23 +116,19 @@ def extensions(app: flask.Flask):
114
116
  :param app: Flask application instance
115
117
  :return: None
116
118
  """
117
-
118
119
  flask_static_digest.init_app(app)
119
120
  if app.config["USE_WEBSOCKETS"]:
120
121
  socketio.init_app(app)
122
+
123
+ Path(app.config["APP_DATA_DIRECTORY"]).mkdir(parents=True, exist_ok=True)
121
124
  db.init_app(app)
122
125
 
123
126
  if app.config["USE_WEBSOCKETS"]:
124
127
  register_handlers(socketio)
125
128
 
126
- # Create the tables within the application context
127
129
  with app.app_context():
128
130
  db.create_all()
129
131
 
130
- # For automatically reflecting table data
131
- # with app.app_context():
132
- # db.reflect()
133
-
134
132
  return None
135
133
 
136
134
 
@@ -197,63 +195,151 @@ def parse_args():
197
195
  parser.add_argument(
198
196
  "--tt-metal-home", help="Specify a TT-Metal home path", default=None
199
197
  )
198
+ parser.add_argument(
199
+ "--host",
200
+ type=str,
201
+ help="Host to bind to (default: auto-detected based on environment)",
202
+ default=None,
203
+ )
204
+ parser.add_argument(
205
+ "--port",
206
+ type=str,
207
+ help="Port to bind to (default: 8000)",
208
+ default=None,
209
+ )
210
+ parser.add_argument(
211
+ "--server",
212
+ action="store_true",
213
+ help="Bind to all network interfaces (0.0.0.0) and enable server mode. Useful for servers and VMs",
214
+ )
215
+ parser.add_argument(
216
+ "-d",
217
+ "--daemon",
218
+ action="store_true",
219
+ help="Run the server as a daemon process",
220
+ )
200
221
  return parser.parse_args()
201
222
 
202
223
 
224
+ def display_mode_info_without_db(config):
225
+ """Display mode information using only config, without initializing database."""
226
+ # Determine if we're in TT-Metal mode
227
+ tt_metal_home = config.TT_METAL_HOME
228
+ is_tt_metal_mode = tt_metal_home is not None
229
+
230
+ if is_tt_metal_mode:
231
+ print("🚀 TT-METAL MODE: Working directly with tt-metal generated directory")
232
+ print(f" TT_METAL_HOME: {tt_metal_home}")
233
+
234
+ profiler_base = Path(tt_metal_home) / "generated" / "ttnn" / "reports"
235
+ performance_base = Path(tt_metal_home) / "generated" / "profiler" / "reports"
236
+
237
+ print(f" Profiler reports: {profiler_base}")
238
+ print(f" Performance reports: {performance_base}")
239
+
240
+ # Validate setup
241
+ if not Path(tt_metal_home).exists():
242
+ print(
243
+ f" ⚠️ Warning: TT_METAL_HOME directory does not exist: {tt_metal_home}"
244
+ )
245
+ elif not (Path(tt_metal_home) / "generated").exists():
246
+ print(f" ⚠️ Warning: TT-Metal generated directory not found")
247
+ elif not profiler_base.exists():
248
+ print(
249
+ f" ⚠️ Warning: Profiler reports directory not found: {profiler_base}"
250
+ )
251
+ elif not performance_base.exists():
252
+ print(
253
+ f" ⚠️ Warning: Performance reports directory not found: {performance_base}"
254
+ )
255
+ else:
256
+ print(f" ✓ TT-Metal setup is valid")
257
+ else:
258
+ print(
259
+ "📁 UPLOAD/SYNC MODE: Using local data directory for uploaded/synced reports"
260
+ )
261
+ print(f" Local directory: {config.LOCAL_DATA_DIRECTORY}")
262
+ print(f" Remote directory: {config.REMOTE_DATA_DIRECTORY}")
263
+
264
+
203
265
  def main():
204
266
 
205
267
  run_command = sys.argv[0].split("/")
206
268
  if run_command[-1] == "ttnn-visualizer":
207
269
  os.environ.setdefault("FLASK_ENV", "production")
208
270
 
209
- config = cast(DefaultConfig, Config())
210
271
  args = parse_args()
211
- instance_id = None
212
272
 
213
- if args.profiler_path or args.performance_path:
214
- app = create_app()
215
- app.app_context().push()
216
- try:
217
- session = create_instance_from_local_paths(
218
- profiler_path=args.profiler_path,
219
- performance_path=args.performance_path,
220
- )
221
- except InvalidReportPath:
222
- sys.exit("Invalid report path")
223
- except InvalidProfilerPath:
224
- sys.exit("Invalid profiler path")
273
+ # Handle host/port CLI overrides
274
+ # Priority: CLI args > env vars > auto-detection (in settings.py)
275
+ # Note: We need to set env vars before creating Config, but also
276
+ # manually update the config object in case it was already instantiated
277
+ if args.host:
278
+ os.environ["HOST"] = args.host
279
+ print(f"🌐 Binding to host: {args.host} (from --host flag)")
280
+ elif args.server:
281
+ os.environ["HOST"] = "0.0.0.0"
282
+ os.environ["SERVER_MODE"] = "true"
283
+ print("🌐 Binding to all interfaces (0.0.0.0) via --server flag")
284
+ print("🖥️ Server mode enabled")
285
+
286
+ if args.port:
287
+ os.environ["PORT"] = args.port
288
+ print(f"🔌 Binding to port: {args.port}")
289
+
290
+ config = cast(DefaultConfig, Config())
291
+
292
+ # Apply CLI overrides directly to config object
293
+ # (Config is a singleton that may have been created before we set env vars)
294
+ if args.host:
295
+ config.HOST = args.host
296
+ elif args.server:
297
+ config.HOST = "0.0.0.0"
298
+ config.SERVER_MODE = True
225
299
 
226
- instance_id = session.instance_id
300
+ if args.port:
301
+ config.PORT = args.port
227
302
 
303
+ # Recalculate GUNICORN_BIND with the updated values
304
+ config.GUNICORN_BIND = f"{config.HOST}:{config.PORT}"
305
+
306
+ instance_id = None
307
+
308
+ # Display mode information first (using config only, no DB needed)
228
309
  if args.tt_metal_home:
310
+ os.environ["TT_METAL_HOME"] = args.tt_metal_home
229
311
  config.TT_METAL_HOME = args.tt_metal_home
230
312
 
231
- # Display mode information
232
- app = create_app()
233
- with app.app_context():
234
- resolver = create_path_resolver(app)
235
- mode_info = resolver.get_mode_info()
236
-
237
- if mode_info["mode"] == "tt_metal":
238
- print(
239
- "🚀 TT-METAL MODE: Working directly with tt-metal generated directory"
313
+ if not os.getenv("APP_DATA_DIRECTORY"):
314
+ config.APP_DATA_DIRECTORY = get_app_data_directory(
315
+ args.tt_metal_home, config.APPLICATION_DIR
240
316
  )
241
- print(f" TT_METAL_HOME: {mode_info['tt_metal_home']}")
242
- print(f" Profiler reports: {mode_info['profiler_base']}")
243
- print(f" Performance reports: {mode_info['performance_base']}")
244
-
245
- # Validate setup
246
- is_valid, message = resolver.validate_tt_metal_setup()
247
- if is_valid:
248
- print(f" ✓ {message}")
249
- else:
250
- print(f" ⚠️ Warning: {message}")
251
- else:
252
- print(
253
- "📁 UPLOAD/SYNC MODE: Using local data directory for uploaded/synced reports"
317
+ # Recalculate database path with new APP_DATA_DIRECTORY
318
+ _db_file_path = str(
319
+ Path(config.APP_DATA_DIRECTORY) / f"ttnn_{config.DB_VERSION}.db"
254
320
  )
255
- print(f" Local directory: {mode_info['local_dir']}")
256
- print(f" Remote directory: {mode_info['remote_dir']}")
321
+ config.SQLALCHEMY_DATABASE_URI = f"sqlite:///{_db_file_path}"
322
+
323
+ display_mode_info_without_db(config)
324
+
325
+ # If profiler/performance paths are provided, create an instance
326
+ # This requires DB access, so we create the app temporarily
327
+ if args.profiler_path or args.performance_path:
328
+ app = create_app()
329
+ with app.app_context():
330
+ try:
331
+ session = create_instance_from_local_paths(
332
+ profiler_path=args.profiler_path,
333
+ performance_path=args.performance_path,
334
+ )
335
+ instance_id = session.instance_id
336
+ except InvalidReportPath:
337
+ sys.exit("Invalid report path")
338
+ except InvalidProfilerPath:
339
+ sys.exit("Invalid profiler path")
340
+
341
+ # Clean up this temporary app - workers will create their own
342
+ del app
257
343
 
258
344
  # Check if DEBUG environment variable is set
259
345
  debug_mode = os.environ.get("DEBUG", "false").lower() == "true"
@@ -262,8 +348,21 @@ def main():
262
348
  for key, value in config.to_dict().items():
263
349
  print(f"{key}={value}")
264
350
 
351
+ # Warn if there's a gunicorn config file in current directory
352
+ if Path("gunicorn.conf.py").exists():
353
+ logger.warning(
354
+ "Found gunicorn.conf.py in current directory - this may override environment settings"
355
+ )
356
+
357
+ gunicorn_cmd, gunicorn_warning = find_gunicorn_path()
358
+
359
+ if gunicorn_warning:
360
+ print(gunicorn_warning)
361
+
265
362
  gunicorn_args = [
266
- "gunicorn",
363
+ gunicorn_cmd,
364
+ "-t",
365
+ config.GUNICORN_TIMEOUT,
267
366
  "-k",
268
367
  config.GUNICORN_WORKER_CLASS,
269
368
  "-w",
@@ -276,7 +375,10 @@ def main():
276
375
  if debug_mode:
277
376
  gunicorn_args.insert(1, "--reload")
278
377
 
279
- if config.LAUNCH_BROWSER_ON_START:
378
+ if args.daemon:
379
+ gunicorn_args.insert(1, "--daemon")
380
+
381
+ if config.LAUNCH_BROWSER_ON_START and not args.daemon:
280
382
  flask_env = os.getenv("FLASK_ENV", "development")
281
383
  port = config.PORT if flask_env == "production" else config.DEV_SERVER_PORT
282
384
  host = config.HOST if flask_env == "production" else config.DEV_SERVER_HOST
@@ -4,6 +4,7 @@
4
4
 
5
5
  import csv
6
6
  import json
7
+ import logging
7
8
  import os
8
9
  import tempfile
9
10
  from io import StringIO
@@ -14,7 +15,9 @@ import pandas as pd
14
15
  import zstd
15
16
  from tt_perf_report import perf_report
16
17
  from ttnn_visualizer.exceptions import DataFormatError
17
- from ttnn_visualizer.models import Instance, RemoteConnection
18
+ from ttnn_visualizer.models import Instance
19
+
20
+ logger = logging.getLogger(__name__)
18
21
 
19
22
 
20
23
  class LocalCSVQueryRunner:
@@ -100,7 +103,6 @@ class NPEQueries:
100
103
 
101
104
  @staticmethod
102
105
  def get_npe_manifest(instance: Instance):
103
-
104
106
  file_path = Path(
105
107
  instance.performance_path,
106
108
  NPEQueries.NPE_FOLDER,
@@ -417,34 +419,71 @@ class OpsPerformanceReportQueries:
417
419
  "raw_op_code",
418
420
  ]
419
421
 
422
+ STACKED_REPORT_COLUMNS = [
423
+ "percent",
424
+ "op_code",
425
+ "device_time_sum_us",
426
+ "ops_count",
427
+ "flops_min",
428
+ "flops_max",
429
+ "flops_mean",
430
+ "flops_std",
431
+ ]
432
+
420
433
  PASSTHROUGH_COLUMNS = {
421
434
  "pm_ideal_ns": "PM IDEAL [ns]",
422
435
  }
423
436
 
424
- DEFAULT_SIGNPOST = None
425
- DEFAULT_IGNORE_SIGNPOSTS = None
437
+ DEFAULT_START_SIGNPOST = None
438
+ DEFAULT_END_SIGNPOST = None
439
+ DEFAULT_IGNORE_SIGNPOSTS = True
426
440
  DEFAULT_MIN_PERCENTAGE = 0.5
427
441
  DEFAULT_ID_RANGE = None
428
442
  DEFAULT_NO_ADVICE = False
429
443
  DEFAULT_TRACING_MODE = False
444
+ DEFAULT_RAW_OP_CODES = True
445
+ DEFAULT_NO_HOST_OPS = False
446
+ DEFAULT_NO_STACKED_REPORT = False
447
+ DEFAULT_NO_STACK_BY_IN0 = True
430
448
 
431
449
  @classmethod
432
- def generate_report(cls, instance):
450
+ def generate_report(cls, instance, **kwargs):
433
451
  raw_csv = OpsPerformanceQueries.get_raw_csv(instance)
434
452
  csv_file = StringIO(raw_csv)
435
453
  csv_output_file = tempfile.mktemp(suffix=".csv")
436
- perf_report.generate_perf_report(
437
- csv_file,
438
- cls.DEFAULT_SIGNPOST,
439
- cls.DEFAULT_IGNORE_SIGNPOSTS,
440
- cls.DEFAULT_MIN_PERCENTAGE,
441
- cls.DEFAULT_ID_RANGE,
442
- csv_output_file,
443
- cls.DEFAULT_NO_ADVICE,
444
- cls.DEFAULT_TRACING_MODE,
445
- True,
446
- True,
447
- )
454
+ csv_stacked_output_file = tempfile.mktemp(suffix=".csv")
455
+ start_signpost = kwargs.get("start_signpost", cls.DEFAULT_START_SIGNPOST)
456
+ end_signpost = kwargs.get("end_signpost", cls.DEFAULT_END_SIGNPOST)
457
+ ignore_signposts = cls.DEFAULT_IGNORE_SIGNPOSTS
458
+ stack_by_in0 = kwargs.get("stack_by_in0", cls.DEFAULT_NO_STACK_BY_IN0)
459
+ no_host_ops = kwargs.get("hide_host_ops", cls.DEFAULT_NO_HOST_OPS)
460
+
461
+ if start_signpost or end_signpost:
462
+ ignore_signposts = False
463
+
464
+ # perf_report currently generates a PNG alongside the CSV using the same temp name - we'll just delete it afterwards
465
+ stacked_png_file = os.path.splitext(csv_output_file)[0] + ".png"
466
+
467
+ try:
468
+ perf_report.generate_perf_report(
469
+ [csv_file],
470
+ start_signpost,
471
+ end_signpost,
472
+ ignore_signposts,
473
+ cls.DEFAULT_MIN_PERCENTAGE,
474
+ cls.DEFAULT_ID_RANGE,
475
+ csv_output_file,
476
+ cls.DEFAULT_NO_ADVICE,
477
+ cls.DEFAULT_TRACING_MODE,
478
+ cls.DEFAULT_RAW_OP_CODES,
479
+ no_host_ops,
480
+ cls.DEFAULT_NO_STACKED_REPORT,
481
+ stack_by_in0,
482
+ csv_stacked_output_file,
483
+ )
484
+ except Exception as e:
485
+ logger.error(f"Error generating performance report: {e}")
486
+ raise DataFormatError(f"Error generating performance report: {e}") from e
448
487
 
449
488
  ops_perf_results = []
450
489
  ops_perf_results_reader = csv.DictReader(StringIO(raw_csv))
@@ -452,34 +491,104 @@ class OpsPerformanceReportQueries:
452
491
  for row in ops_perf_results_reader:
453
492
  ops_perf_results.append(row)
454
493
 
494
+ # Returns a list of unique signposts in the order they appear
495
+ # TODO: Signpost names are not unique but tt-perf-report treats them as such
496
+ captured_signposts = set()
497
+ signposts = []
498
+ for index, row in enumerate(ops_perf_results):
499
+ if row.get("OP TYPE") == "signpost":
500
+ op_code = row["OP CODE"]
501
+ op_id = index + 2 # Match IDs with row numbers in ops perf results csv
502
+ if not any(s["op_code"] == op_code for s in signposts):
503
+ captured_signposts.add(op_code)
504
+ signposts.append(
505
+ {
506
+ "id": op_id,
507
+ "op_code": op_code,
508
+ }
509
+ )
510
+
455
511
  report = []
456
512
 
457
- try:
458
- with open(csv_output_file, newline="") as csvfile:
459
- reader = csv.reader(csvfile, delimiter=",")
460
- next(reader, None)
461
- for row in reader:
462
- processed_row = {
463
- column: row[index]
464
- for index, column in enumerate(cls.REPORT_COLUMNS)
465
- if index < len(row)
466
- }
467
- if "advice" in processed_row and processed_row["advice"]:
468
- processed_row["advice"] = processed_row["advice"].split(" • ")
469
- else:
470
- processed_row["advice"] = []
471
-
472
- for key, value in cls.PASSTHROUGH_COLUMNS.items():
513
+ if os.path.exists(csv_output_file):
514
+ try:
515
+ with open(csv_output_file, newline="") as csvfile:
516
+ reader = csv.reader(csvfile, delimiter=",")
517
+ next(reader, None)
518
+ for row in reader:
473
519
  op_id = int(row[0])
474
- idx = (
475
- op_id - 2
476
- ) # IDs in result column one correspond to row numbers in ops perf results csv
477
- processed_row[key] = ops_perf_results[idx][value]
478
-
479
- report.append(processed_row)
480
- except csv.Error as e:
481
- raise DataFormatError() from e
482
- finally:
483
- os.unlink(csv_output_file)
484
-
485
- return report
520
+ # IDs in result column one correspond to row numbers in ops perf results csv
521
+ idx = op_id - 2
522
+
523
+ processed_row = {
524
+ column: row[index]
525
+ for index, column in enumerate(cls.REPORT_COLUMNS)
526
+ if index < len(row)
527
+ }
528
+
529
+ if "advice" in processed_row and processed_row["advice"]:
530
+ processed_row["advice"] = processed_row["advice"].split(
531
+ " • "
532
+ )
533
+ else:
534
+ processed_row["advice"] = []
535
+
536
+ for key, value in cls.PASSTHROUGH_COLUMNS.items():
537
+ if 0 <= idx < len(ops_perf_results):
538
+
539
+ processed_row[key] = ops_perf_results[idx][value]
540
+ else:
541
+ processed_row[key] = None
542
+
543
+ # Get the op type from the raw file for this row as it is not returned from tt-perf-report
544
+ if 0 <= idx < len(ops_perf_results):
545
+ processed_row["op_type"] = ops_perf_results[idx].get(
546
+ "OP TYPE"
547
+ )
548
+ else:
549
+ processed_row["op_type"] = None
550
+
551
+ report.append(processed_row)
552
+ except csv.Error as e:
553
+ raise DataFormatError() from e
554
+ finally:
555
+ os.unlink(csv_output_file)
556
+
557
+ stacked_report = []
558
+
559
+ if os.path.exists(csv_stacked_output_file):
560
+ try:
561
+ with open(csv_stacked_output_file, newline="") as csvfile:
562
+ reader = csv.reader(csvfile, delimiter=",")
563
+ next(reader, None)
564
+
565
+ for row in reader:
566
+ processed_row = {
567
+ column: row[index]
568
+ for index, column in enumerate(cls.STACKED_REPORT_COLUMNS)
569
+ if index < len(row)
570
+ }
571
+
572
+ if "op_code" in processed_row and any(
573
+ processed_row["op_code"] in signpost["op_code"]
574
+ for signpost in signposts
575
+ ):
576
+ processed_row["op_type"] = "signpost"
577
+ else:
578
+ processed_row["op_type"] = "unknown"
579
+
580
+ stacked_report.append(processed_row)
581
+ except csv.Error as e:
582
+ raise DataFormatError() from e
583
+ finally:
584
+ os.unlink(csv_stacked_output_file)
585
+ if os.path.exists(stacked_png_file):
586
+ os.unlink(stacked_png_file)
587
+
588
+ stacked_report.append(processed_row)
589
+
590
+ return {
591
+ "report": report,
592
+ "stacked_report": stacked_report,
593
+ "signposts": signposts,
594
+ }
@@ -14,7 +14,6 @@ from ttnn_visualizer.exceptions import (
14
14
  NoProjectsException,
15
15
  NoValidConnectionsError,
16
16
  RemoteConnectionException,
17
- RemoteSqliteException,
18
17
  SSHException,
19
18
  )
20
19
  from ttnn_visualizer.instances import get_or_create_instance
@@ -115,14 +114,6 @@ def remote_exception_handler(func):
115
114
  message=user_message,
116
115
  )
117
116
 
118
- except RemoteSqliteException as err:
119
- current_app.logger.error(f"Remote Sqlite exception: {str(err)}")
120
- message = err.message
121
- if "No such file" in str(err):
122
- message = "Unable to open SQLite binary, check path"
123
- raise RemoteConnectionException(
124
- status=ConnectionTestStates.FAILED, message=message
125
- )
126
117
  except IOError as err:
127
118
  message = f"Error opening remote folder: {str(err)}"
128
119
  if "Name or service not known" in str(err):
@@ -56,13 +56,6 @@ class NoProjectsException(RemoteConnectionException):
56
56
  pass
57
57
 
58
58
 
59
- class RemoteSqliteException(Exception):
60
- def __init__(self, message, status):
61
- super().__init__(message)
62
- self.message = message
63
- self.status = status
64
-
65
-
66
59
  class DatabaseFileNotFoundException(Exception):
67
60
  pass
68
61
 
ttnn_visualizer/models.py CHANGED
@@ -154,6 +154,26 @@ class StackTrace(SerializeableDataclass):
154
154
  stack_trace: str
155
155
 
156
156
 
157
+ @dataclasses.dataclass
158
+ class ErrorRecord(SerializeableDataclass):
159
+ operation_id: int
160
+ operation_name: str
161
+ error_type: str
162
+ error_message: str
163
+ stack_trace: str
164
+ timestamp: str
165
+
166
+ def to_nested_dict(self) -> dict:
167
+ """
168
+ Returns a dictionary representation without operation_id and operation_name.
169
+ Use this when the error is nested under an operation to avoid redundancy.
170
+ """
171
+ result = self.to_dict()
172
+ result.pop("operation_id", None)
173
+ result.pop("operation_name", None)
174
+ return result
175
+
176
+
157
177
  # Non Data Models
158
178
 
159
179
 
@@ -169,7 +189,6 @@ class RemoteConnection(SerializeableModel):
169
189
  port: int = Field(ge=1, le=65535)
170
190
  profilerPath: str
171
191
  performancePath: Optional[str] = None
172
- sqliteBinaryPath: Optional[str] = None
173
192
 
174
193
 
175
194
  class StatusMessage(SerializeableModel):
@@ -12,6 +12,7 @@ from ttnn_visualizer.models import (
12
12
  BufferPage,
13
13
  Device,
14
14
  DeviceOperation,
15
+ ErrorRecord,
15
16
  InputTensor,
16
17
  Instance,
17
18
  Operation,
@@ -156,6 +157,13 @@ class DatabaseQueries:
156
157
  operation_id, stack_trace = row
157
158
  yield StackTrace(operation_id, stack_trace=stack_trace)
158
159
 
160
+ def query_error_records(
161
+ self, filters: Optional[Dict[str, Any]] = None
162
+ ) -> Generator[ErrorRecord, None, None]:
163
+ rows = self._query_table("errors", filters)
164
+ for row in rows:
165
+ yield ErrorRecord(*row)
166
+
159
167
  def query_tensor_comparisons(
160
168
  self, local: bool = True, filters: Optional[Dict[str, Any]] = None
161
169
  ) -> Generator[TensorComparisonRecord, None, None]: