From 97bc182cf2de730ad40ddd039075785a73636572 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 8 Oct 2024 13:58:45 -0300 Subject: [PATCH 01/67] Adds query for buffer_pages and tests for query --- backend/ttnn_visualizer/queries.py | 33 ++++++++++-- backend/ttnn_visualizer/tests/test_queries.py | 50 +++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/backend/ttnn_visualizer/queries.py b/backend/ttnn_visualizer/queries.py index d25e8e44..cb028297 100644 --- a/backend/ttnn_visualizer/queries.py +++ b/backend/ttnn_visualizer/queries.py @@ -150,15 +150,40 @@ def query_buffers(self, operation_id: int) -> Generator[Buffer, None, None]: cursor.close() def query_buffer_pages( - self, operation_id: int + self, + operation_id: Optional[int] = None, + address: Optional[int] = None, + buffer_type: Optional[int] = None, ) -> Generator[BufferPage, None, None]: cursor = self._get_cursor() try: - cursor.execute( - "SELECT * FROM buffer_pages WHERE operation_id = ?", (operation_id,) - ) + # Check if the buffer_pages table exists + if not self._check_table_exists(cursor, "buffer_pages"): + return + + # noinspection SqlConstantExpression + query = "SELECT * FROM buffer_pages WHERE 1=1" + params = [] + + # Add optional conditions + if operation_id is not None: + query += " AND operation_id = ?" + params.append(operation_id) + + if address is not None: + query += " AND address = ?" + params.append(address) + + if buffer_type is not None: + query += " AND buffer_type = ?" + params.append(buffer_type) + + # Execute the query + cursor.execute(query, params) + for row in cursor.fetchall(): yield BufferPage(*row) + finally: cursor.close() diff --git a/backend/ttnn_visualizer/tests/test_queries.py b/backend/ttnn_visualizer/tests/test_queries.py index 28391ee1..6efde40d 100644 --- a/backend/ttnn_visualizer/tests/test_queries.py +++ b/backend/ttnn_visualizer/tests/test_queries.py @@ -11,6 +11,8 @@ OperationArgument, StackTrace, ProducersConsumers, + BufferPage, + BufferType, ) from ttnn_visualizer.queries import DatabaseQueries @@ -92,6 +94,18 @@ def _create_tables(self): name text, duration float ); + CREATE TABLE buffer_pages ( + operation_id INT, + device_id INT, + address INT, + core_y INT, + core_x INT, + bank_id INT, + page_index INT, + page_address INT, + page_size INT, + buffer_type INT + ); """ self.connection.executescript(schema) @@ -126,6 +140,42 @@ def test_query_device_operations_by_operation_id(self): no_operation = self.db_queries.query_device_operations_by_operation_id(999) self.assertIsNone(no_operation) + def test_query_buffer_pages(self): + # Insert sample data into buffer_pages table + self.connection.execute( + "INSERT INTO buffer_pages (operation_id, device_id, address, core_y, core_x, bank_id, page_index, page_address, page_size, buffer_type) " + "VALUES (1, 1, 1234, 0, 0, 1, 0, 1000, 4096, 0)" + ) + + # Query without any filters + buffer_pages = list(self.db_queries.query_buffer_pages()) + self.assertEqual(len(buffer_pages), 1) + + # Validate the returned buffer page + buffer_page = buffer_pages[0] + self.assertIsInstance(buffer_page, BufferPage) + self.assertEqual(buffer_page.operation_id, 1) + self.assertEqual(buffer_page.address, 1234) + self.assertEqual(buffer_page.buffer_type, BufferType(0).value) + + # Query with filter by operation_id + buffer_pages = list(self.db_queries.query_buffer_pages(operation_id=1)) + self.assertEqual(len(buffer_pages), 1) + + # Query with filter by address + buffer_pages = list(self.db_queries.query_buffer_pages(address=1234)) + self.assertEqual(len(buffer_pages), 1) + + # Query with filter by buffer_type + buffer_pages = list( + self.db_queries.query_buffer_pages(buffer_type=BufferType(0).value) + ) + self.assertEqual(len(buffer_pages), 1) + + # Query with a non-matching filter + buffer_pages = list(self.db_queries.query_buffer_pages(operation_id=9999)) + self.assertEqual(len(buffer_pages), 0) + def test_query_buffers(self): self.connection.execute("INSERT INTO buffers VALUES (1, 1, 0, 1024, 0)") buffers = list(self.db_queries.query_buffers(1)) From 9f9a9fcf315854069a5131cf2934fc648ee07cd2 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 8 Oct 2024 14:04:29 -0300 Subject: [PATCH 02/67] Adds view for querying buffer_pages --- backend/ttnn_visualizer/views.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/backend/ttnn_visualizer/views.py b/backend/ttnn_visualizer/views.py index c267d642..d994ae37 100644 --- a/backend/ttnn_visualizer/views.py +++ b/backend/ttnn_visualizer/views.py @@ -16,6 +16,7 @@ serialize_operations, serialize_tensors, serialize_operation, + serialize_buffer_pages, ) from ttnn_visualizer.sessions import update_tab_session from ttnn_visualizer.sftp_operations import ( @@ -162,6 +163,28 @@ def buffer_detail(report_path): return dataclasses.asdict(buffer) +@api.route("/buffer-pages", methods=["GET"]) +@with_report_path +@timer +def buffer_pages(report_path): + address = request.args.get("address") + operation_id = request.args.get("operation_id") + buffer_type = request.args.get("buffer_type", "") + + if buffer_type and str.isdigit(buffer_type): + buffer_type = int(buffer_type) + else: + buffer_type = None + + if not address or not operation_id: + return Response(status=HTTPStatus.BAD_REQUEST) + + with DatabaseQueries(report_path) as db: + buffers = list(db.query_buffer_pages(operation_id, address, buffer_type)) + devices = list(db.query_devices()) + return serialize_buffer_pages(buffers, devices) + + @api.route("/tensors/", methods=["GET"]) @with_report_path @timer From 701a37609cdbe9c40c8b44449d39dc435d042ac2 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 8 Oct 2024 14:04:49 -0300 Subject: [PATCH 03/67] Adds method for serializing buffer pages --- backend/ttnn_visualizer/serializers.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/backend/ttnn_visualizer/serializers.py b/backend/ttnn_visualizer/serializers.py index dcba5318..e0f2c734 100644 --- a/backend/ttnn_visualizer/serializers.py +++ b/backend/ttnn_visualizer/serializers.py @@ -1,6 +1,8 @@ import dataclasses from collections import defaultdict +from ttnn_visualizer.models import BufferType + def serialize_operations( inputs, @@ -85,6 +87,30 @@ def attach_producers_consumers(values): return inputs_dict, outputs_dict +def serialize_buffer_pages(buffer_pages, devices): + # Collect device-specific data if needed + l1_sizes = [d.worker_l1_size for d in devices] + + # Serialize each buffer page to a dictionary using dataclasses.asdict + buffer_pages_list = [dataclasses.asdict(page) for page in buffer_pages] + + # Optionally, modify or adjust the serialized data as needed + for page_data in buffer_pages_list: + # Set a custom id field if needed + page_data["id"] = f"{page_data['operation_id']}_{page_data['page_index']}" + + # If the buffer_type is handled by an enum, adjust it similarly to your BufferPage model + if "buffer_type" in page_data and isinstance( + page_data["buffer_type"], BufferType + ): + page_data["buffer_type"] = page_data["buffer_type"].value + + return { + "buffer_pages": buffer_pages_list, + "l1_sizes": l1_sizes, + } + + def serialize_operation( buffers, inputs, From 12e7a3cc1cbc05d498a11cefa3d59a73e7fff01b Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 8 Oct 2024 14:08:18 -0300 Subject: [PATCH 04/67] Adds tests for serializing buffer pages --- backend/ttnn_visualizer/serializers.py | 4 +- .../ttnn_visualizer/tests/test_serializers.py | 65 +++++++++++++++++++ backend/ttnn_visualizer/views.py | 3 +- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/backend/ttnn_visualizer/serializers.py b/backend/ttnn_visualizer/serializers.py index e0f2c734..6093c920 100644 --- a/backend/ttnn_visualizer/serializers.py +++ b/backend/ttnn_visualizer/serializers.py @@ -87,9 +87,8 @@ def attach_producers_consumers(values): return inputs_dict, outputs_dict -def serialize_buffer_pages(buffer_pages, devices): +def serialize_buffer_pages(buffer_pages): # Collect device-specific data if needed - l1_sizes = [d.worker_l1_size for d in devices] # Serialize each buffer page to a dictionary using dataclasses.asdict buffer_pages_list = [dataclasses.asdict(page) for page in buffer_pages] @@ -107,7 +106,6 @@ def serialize_buffer_pages(buffer_pages, devices): return { "buffer_pages": buffer_pages_list, - "l1_sizes": l1_sizes, } diff --git a/backend/ttnn_visualizer/tests/test_serializers.py b/backend/ttnn_visualizer/tests/test_serializers.py index 2282d540..5f50a85d 100644 --- a/backend/ttnn_visualizer/tests/test_serializers.py +++ b/backend/ttnn_visualizer/tests/test_serializers.py @@ -12,12 +12,14 @@ DeviceOperation, Buffer, BufferType, + BufferPage, ) from ttnn_visualizer.serializers import ( serialize_operations, serialize_inputs_outputs, serialize_operation, serialize_tensors, + serialize_buffer_pages, ) @@ -272,6 +274,69 @@ def test_serialize_operation(self): self.assertEqual(result, expected) + def test_serialize_buffer_pages(self): + buffer_pages = [ + BufferPage( + operation_id=1, + device_id=1, + address=1234, + core_y=0, + core_x=0, + bank_id=1, + page_index=0, + page_address=1000, + page_size=4096, + buffer_type=BufferType.DRAM, + ), + BufferPage( + operation_id=2, + device_id=2, + address=5678, + core_y=1, + core_x=1, + bank_id=2, + page_index=1, + page_address=2000, + page_size=8192, + buffer_type=BufferType.L1, + ), + ] + + result = serialize_buffer_pages(buffer_pages) + + expected = { + "buffer_pages": [ + { + "operation_id": 1, + "device_id": 1, + "address": 1234, + "core_y": 0, + "core_x": 0, + "bank_id": 1, + "page_index": 0, + "page_address": 1000, + "page_size": 4096, + "buffer_type": 0, + "id": "1_0", + }, + { + "operation_id": 2, + "device_id": 2, + "address": 5678, + "core_y": 1, + "core_x": 1, + "bank_id": 2, + "page_index": 1, + "page_address": 2000, + "page_size": 8192, + "buffer_type": 1, + "id": "2_1", + }, + ] + } + + self.assertEqual(result, expected) + def test_serialize_tensors(self): tensors = [ Tensor( diff --git a/backend/ttnn_visualizer/views.py b/backend/ttnn_visualizer/views.py index d994ae37..40cddc74 100644 --- a/backend/ttnn_visualizer/views.py +++ b/backend/ttnn_visualizer/views.py @@ -181,8 +181,7 @@ def buffer_pages(report_path): with DatabaseQueries(report_path) as db: buffers = list(db.query_buffer_pages(operation_id, address, buffer_type)) - devices = list(db.query_devices()) - return serialize_buffer_pages(buffers, devices) + return serialize_buffer_pages(buffers) @api.route("/tensors/", methods=["GET"]) From 8b614470f35d828b89dc3ff00696842d5aa0c76e Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 8 Oct 2024 14:14:07 -0300 Subject: [PATCH 05/67] Remove 404 on missing params --- backend/ttnn_visualizer/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/ttnn_visualizer/views.py b/backend/ttnn_visualizer/views.py index 40cddc74..79dd84ba 100644 --- a/backend/ttnn_visualizer/views.py +++ b/backend/ttnn_visualizer/views.py @@ -176,9 +176,6 @@ def buffer_pages(report_path): else: buffer_type = None - if not address or not operation_id: - return Response(status=HTTPStatus.BAD_REQUEST) - with DatabaseQueries(report_path) as db: buffers = list(db.query_buffer_pages(operation_id, address, buffer_type)) return serialize_buffer_pages(buffers) From 97da25d7257651b3bb638b9caff1483d8cebb6b4 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 8 Oct 2024 15:05:02 -0300 Subject: [PATCH 06/67] Remove table check on buffer_pages table --- backend/ttnn_visualizer/queries.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/ttnn_visualizer/queries.py b/backend/ttnn_visualizer/queries.py index cb028297..b18f1728 100644 --- a/backend/ttnn_visualizer/queries.py +++ b/backend/ttnn_visualizer/queries.py @@ -157,10 +157,6 @@ def query_buffer_pages( ) -> Generator[BufferPage, None, None]: cursor = self._get_cursor() try: - # Check if the buffer_pages table exists - if not self._check_table_exists(cursor, "buffer_pages"): - return - # noinspection SqlConstantExpression query = "SELECT * FROM buffer_pages WHERE 1=1" params = [] From 96e2e34bb133623eb30ef6ec63e25e2c470c4a68 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 8 Oct 2024 15:06:15 -0300 Subject: [PATCH 07/67] Removes unused database create script --- backend/ttnn_visualizer/database.py | 91 ----------------------------- 1 file changed, 91 deletions(-) delete mode 100644 backend/ttnn_visualizer/database.py diff --git a/backend/ttnn_visualizer/database.py b/backend/ttnn_visualizer/database.py deleted file mode 100644 index 06ca8999..00000000 --- a/backend/ttnn_visualizer/database.py +++ /dev/null @@ -1,91 +0,0 @@ -import sqlite3 -from logging import getLogger - -logger = getLogger(__name__) - - -def create_update_database(sqlite_db_path): - """ - Creates or updates database with all tables - :param sqlite_db_path Path to target SQLite database - :return: - """ - sqlite_connection = sqlite3.connect(sqlite_db_path) - logger.info("Creating/updating SQLite database") - cursor = sqlite_connection.cursor() - cursor.execute( - """CREATE TABLE IF NOT EXISTS devices - ( - device_id int, - num_y_cores int, - num_x_cores int, - num_y_compute_cores int, - num_x_compute_cores int, - worker_l1_size int, - l1_num_banks int, - l1_bank_size int, - address_at_first_l1_bank int, - address_at_first_l1_cb_buffer int, - num_banks_per_storage_core int, - num_compute_cores int, - num_storage_cores int, - total_l1_memory int, - total_l1_for_tensors int, - total_l1_for_interleaved_buffers int, - total_l1_for_sharded_buffers int, - cb_limit int - )""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS tensors - (tensor_id int UNIQUE, shape text, dtype text, layout text, memory_config text, device_id int, address int, buffer_type int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS local_tensor_comparison_records - (tensor_id int UNIQUE, golden_tensor_id int, matches bool, desired_pcc bool, actual_pcc float)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS global_tensor_comparison_records - (tensor_id int UNIQUE, golden_tensor_id int, matches bool, desired_pcc bool, actual_pcc float)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS operations - (operation_id int UNIQUE, name text, duration float)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS operation_arguments - (operation_id int, name text, value text)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS stack_traces - (operation_id int, stack_trace text)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS input_tensors - (operation_id int, input_index int, tensor_id int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS output_tensors - (operation_id int, output_index int, tensor_id int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS buffers - (operation_id int, device_id int, address int, max_size_per_bank int, buffer_type int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS buffer_pages - (operation_id int, device_id int, address int, core_y int, core_x int, bank_id int, page_index int, page_address int, page_size int, buffer_type int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS nodes - (operation_id int, unique_id int, node_operation_id int, name text)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS edges - (operation_id int, source_unique_id int, sink_unique_id int, source_output_index int, sink_input_index int, key int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS captured_graph - (operation_id int, captured_graph text)""" - ) - sqlite_connection.commit() From 74c435f66dc079ce42422aeb30954fdf62792ce3 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 8 Oct 2024 15:11:46 -0300 Subject: [PATCH 08/67] Removes unused imports --- backend/ttnn_visualizer/app.py | 8 ++++---- backend/ttnn_visualizer/config/gunicorn.py | 3 ++- backend/ttnn_visualizer/sessions.py | 4 ++-- backend/ttnn_visualizer/tests/test_queries.py | 4 ++-- backend/ttnn_visualizer/tests/test_serializers.py | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/backend/ttnn_visualizer/app.py b/backend/ttnn_visualizer/app.py index b2c1146b..14c5dcbf 100644 --- a/backend/ttnn_visualizer/app.py +++ b/backend/ttnn_visualizer/app.py @@ -1,15 +1,15 @@ import logging -import shutil from os import environ from pathlib import Path -from flask import Flask import flask +from dotenv import load_dotenv +from flask import Flask +from flask_cors import CORS from werkzeug.debug import DebuggedApplication from werkzeug.middleware.proxy_fix import ProxyFix -from flask_cors import CORS + from ttnn_visualizer import settings -from dotenv import load_dotenv from ttnn_visualizer.sessions import init_sessions, CustomRequest, init_session_db diff --git a/backend/ttnn_visualizer/config/gunicorn.py b/backend/ttnn_visualizer/config/gunicorn.py index b05d0663..cbff5a4f 100644 --- a/backend/ttnn_visualizer/config/gunicorn.py +++ b/backend/ttnn_visualizer/config/gunicorn.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -import multiprocessing import os from pathlib import Path + from dotenv import load_dotenv + from ttnn_visualizer.utils import str_to_bool # Load dotenv from root directory diff --git a/backend/ttnn_visualizer/sessions.py b/backend/ttnn_visualizer/sessions.py index 0f40ba5f..944eb259 100644 --- a/backend/ttnn_visualizer/sessions.py +++ b/backend/ttnn_visualizer/sessions.py @@ -1,10 +1,10 @@ -import sqlite3 import json +import sqlite3 import threading from pathlib import Path from typing import TypedDict, cast -from flask import request, jsonify, g, Request +from flask import request, g, Request # Path to the SQLite database for sessions DATABASE = "sessions.db" diff --git a/backend/ttnn_visualizer/tests/test_queries.py b/backend/ttnn_visualizer/tests/test_queries.py index 6efde40d..7b923859 100644 --- a/backend/ttnn_visualizer/tests/test_queries.py +++ b/backend/ttnn_visualizer/tests/test_queries.py @@ -1,5 +1,6 @@ -import unittest import sqlite3 +import unittest + from ttnn_visualizer.models import ( Operation, Device, @@ -10,7 +11,6 @@ OutputTensor, OperationArgument, StackTrace, - ProducersConsumers, BufferPage, BufferType, ) diff --git a/backend/ttnn_visualizer/tests/test_serializers.py b/backend/ttnn_visualizer/tests/test_serializers.py index 5f50a85d..63b1b6d7 100644 --- a/backend/ttnn_visualizer/tests/test_serializers.py +++ b/backend/ttnn_visualizer/tests/test_serializers.py @@ -1,5 +1,5 @@ import unittest -from collections import defaultdict + from ttnn_visualizer.models import ( Operation, OperationArgument, From fb3c9a95f9d78b0d1acd94f42fc34a176a27ec3f Mon Sep 17 00:00:00 2001 From: David Blundell Date: Wed, 9 Oct 2024 10:56:39 -0400 Subject: [PATCH 09/67] Added basic zoom --- src/components/BufferSummaryPlotRenderer.tsx | 32 ++++++++++++++++++-- src/components/BufferSummaryRow.tsx | 9 ++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/components/BufferSummaryPlotRenderer.tsx b/src/components/BufferSummaryPlotRenderer.tsx index 764da96e..1f95b9a1 100644 --- a/src/components/BufferSummaryPlotRenderer.tsx +++ b/src/components/BufferSummaryPlotRenderer.tsx @@ -5,6 +5,7 @@ import { UIEvent, useMemo, useRef, useState } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import classNames from 'classnames'; +import { Switch } from '@blueprintjs/core'; import { BufferSummaryAxisConfiguration } from '../definitions/PlotConfigurations'; import { BuffersByOperationData, useBuffers, useDevices, useOperationsList } from '../hooks/useAPI'; import { BufferType } from '../model/BufferType'; @@ -20,6 +21,7 @@ const TOTAL_SHADE_HEIGHT = 20; // Height in px of 'scroll-shade' pseudo elements function BufferSummaryPlotRenderer() { const [hasScrolledFromTop, setHasScrolledFromTop] = useState(false); const [hasScrolledToBottom, setHasScrolledToBottom] = useState(false); + const [isZoomedIn, setIsZoomedIn] = useState(false); const { data: buffersByOperation, isLoading: isLoadingBuffers } = useBuffers(BufferType.L1); const { data: operationsList, isLoading: isLoadingOperations } = useOperationsList(); const { data: devices, isLoading: isLoadingDevices } = useDevices(); @@ -31,6 +33,20 @@ function BufferSummaryPlotRenderer() { // Will need refactoring to handle multiple devices const memorySize = !isLoadingDevices && devices ? devices[0].worker_l1_size : 0; + const zoomedMemorySize = useMemo(() => { + const sortedBufferSizes = buffersByOperation + ?.map((operation) => operation.buffers.map((b) => ({ ...b, operationId: operation.id }))) + .flat() + .sort((a, b) => b.address - a.address); + + return sortedBufferSizes && sortedBufferSizes.length > 1 + ? [sortedBufferSizes.at(-1)?.address, sortedBufferSizes[0].address + sortedBufferSizes[0].size] + : [0, memorySize]; + }, [buffersByOperation, memorySize]); + + const zoomedMemorySizeStart = zoomedMemorySize[0] || 0; + const zoomedMemorySizeEnd = zoomedMemorySize[1] || memorySize; + const tensorList = useMemo( () => createHistoricalTensorList(operationsList, buffersByOperation), [operationsList, buffersByOperation], @@ -53,6 +69,14 @@ function BufferSummaryPlotRenderer() { return buffersByOperation && !isLoadingBuffers && !isLoadingOperations && !isLoadingDevices && tensorList ? (
+ { + setIsZoomedIn(!isZoomedIn); + }} + /> +

Memory Address

@@ -71,8 +95,9 @@ function BufferSummaryPlotRenderer() { }, ], ]} - isZoomedIn={false} - memorySize={memorySize} + isZoomedIn={isZoomedIn} + memorySize={isZoomedIn ? zoomedMemorySizeEnd : memorySize} + plotZoomRange={isZoomedIn ? [zoomedMemorySizeStart, zoomedMemorySizeEnd] : [0, memorySize]} configuration={BufferSummaryAxisConfiguration} />
@@ -111,7 +136,8 @@ function BufferSummaryPlotRenderer() {

{operation.id}

diff --git a/src/components/BufferSummaryRow.tsx b/src/components/BufferSummaryRow.tsx index bf7be17a..ed4bc4f0 100644 --- a/src/components/BufferSummaryRow.tsx +++ b/src/components/BufferSummaryRow.tsx @@ -10,6 +10,7 @@ interface BufferSummaryRowProps { buffers: Buffer[]; operationId: number; memorySize: number; + memoryStart?: number; } interface Buffer { @@ -21,12 +22,14 @@ interface Buffer { const SCALE = 100; -function BufferSummaryRow({ buffers, operationId, memorySize }: BufferSummaryRowProps) { +function BufferSummaryRow({ buffers, operationId, memorySize, memoryStart = 0 }: BufferSummaryRowProps) { + const computedMemorySize = memorySize - memoryStart; + return (
{buffers.map((buffer: Buffer) => { - const size = (buffer.size / memorySize) * SCALE; - const position = (buffer.address / memorySize) * SCALE; + const size = (buffer.size / computedMemorySize) * SCALE; + const position = ((buffer.address - memoryStart) / computedMemorySize) * SCALE; return (
Date: Wed, 9 Oct 2024 12:31:56 -0400 Subject: [PATCH 10/67] Add memory padding --- src/components/BufferSummaryPlotRenderer.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/BufferSummaryPlotRenderer.tsx b/src/components/BufferSummaryPlotRenderer.tsx index 1f95b9a1..b78961cb 100644 --- a/src/components/BufferSummaryPlotRenderer.tsx +++ b/src/components/BufferSummaryPlotRenderer.tsx @@ -17,6 +17,7 @@ import { HistoricalTensor, Operation, Tensor } from '../model/Graph'; const PLACEHOLDER_ARRAY_SIZE = 30; const OPERATION_EL_HEIGHT = 20; // Height in px of each list item const TOTAL_SHADE_HEIGHT = 20; // Height in px of 'scroll-shade' pseudo elements +const MEMORY_ZOOM_PADDING_RATIO = 0.01; function BufferSummaryPlotRenderer() { const [hasScrolledFromTop, setHasScrolledFromTop] = useState(false); @@ -46,6 +47,7 @@ function BufferSummaryPlotRenderer() { const zoomedMemorySizeStart = zoomedMemorySize[0] || 0; const zoomedMemorySizeEnd = zoomedMemorySize[1] || memorySize; + const memoryPadding = (zoomedMemorySizeEnd - zoomedMemorySizeStart) * MEMORY_ZOOM_PADDING_RATIO; const tensorList = useMemo( () => createHistoricalTensorList(operationsList, buffersByOperation), @@ -97,7 +99,11 @@ function BufferSummaryPlotRenderer() { ]} isZoomedIn={isZoomedIn} memorySize={isZoomedIn ? zoomedMemorySizeEnd : memorySize} - plotZoomRange={isZoomedIn ? [zoomedMemorySizeStart, zoomedMemorySizeEnd] : [0, memorySize]} + plotZoomRange={ + isZoomedIn + ? [zoomedMemorySizeStart + memoryPadding, zoomedMemorySizeEnd + memoryPadding] + : [0, memorySize] + } configuration={BufferSummaryAxisConfiguration} />
@@ -138,6 +144,7 @@ function BufferSummaryPlotRenderer() { operationId={operation.id} memorySize={isZoomedIn ? zoomedMemorySizeEnd : memorySize} memoryStart={isZoomedIn ? zoomedMemorySizeStart : 0} + memoryPadding={memoryPadding} />

{operation.id}

From 027cba2c75ad68d2bb164f07cfaef285f007a7ad Mon Sep 17 00:00:00 2001 From: David Blundell Date: Wed, 9 Oct 2024 12:39:35 -0400 Subject: [PATCH 11/67] Refined padding approach --- src/components/BufferSummaryPlotRenderer.tsx | 5 ++--- src/components/BufferSummaryRow.tsx | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/BufferSummaryPlotRenderer.tsx b/src/components/BufferSummaryPlotRenderer.tsx index b78961cb..a2ae02ba 100644 --- a/src/components/BufferSummaryPlotRenderer.tsx +++ b/src/components/BufferSummaryPlotRenderer.tsx @@ -142,9 +142,8 @@ function BufferSummaryPlotRenderer() {

{operation.id}

diff --git a/src/components/BufferSummaryRow.tsx b/src/components/BufferSummaryRow.tsx index ed4bc4f0..7a2ddd03 100644 --- a/src/components/BufferSummaryRow.tsx +++ b/src/components/BufferSummaryRow.tsx @@ -40,6 +40,8 @@ function BufferSummaryRow({ buffers, operationId, memorySize, memoryStart = 0 }: left: `${position}%`, backgroundColor: getBufferColor(buffer.address), }} + data-address={buffer.address} + data-size={buffer.size} /> ); })} From c21580954b76c74b750766da7bb53355d75c5038 Mon Sep 17 00:00:00 2001 From: David Blundell Date: Wed, 9 Oct 2024 12:39:47 -0400 Subject: [PATCH 12/67] Removed debug code --- src/components/BufferSummaryRow.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/BufferSummaryRow.tsx b/src/components/BufferSummaryRow.tsx index 7a2ddd03..ed4bc4f0 100644 --- a/src/components/BufferSummaryRow.tsx +++ b/src/components/BufferSummaryRow.tsx @@ -40,8 +40,6 @@ function BufferSummaryRow({ buffers, operationId, memorySize, memoryStart = 0 }: left: `${position}%`, backgroundColor: getBufferColor(buffer.address), }} - data-address={buffer.address} - data-size={buffer.size} /> ); })} From 4c285f8539f96e4c3a3d51803bbfb964c9016f2b Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 9 Oct 2024 14:57:25 -0300 Subject: [PATCH 13/67] Adds flask-sqlalchemy and flask-session dependencies --- backend/ttnn_visualizer/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/ttnn_visualizer/requirements.txt b/backend/ttnn_visualizer/requirements.txt index 5db05a89..5dffabd4 100644 --- a/backend/ttnn_visualizer/requirements.txt +++ b/backend/ttnn_visualizer/requirements.txt @@ -8,5 +8,7 @@ pydantic_core==2.18.4 flask_static_digest==0.4.1 setuptools==65.5.0 python-dotenv==1.0.1 +flask-sqlalchemy +flask-session wheel build From 94bde8da9d4b2d7ed069fed12697e42221428c46 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 9 Oct 2024 14:58:24 -0300 Subject: [PATCH 14/67] Adds settings for app data database --- backend/ttnn_visualizer/settings.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/ttnn_visualizer/settings.py b/backend/ttnn_visualizer/settings.py index 4adf888c..677ede9e 100644 --- a/backend/ttnn_visualizer/settings.py +++ b/backend/ttnn_visualizer/settings.py @@ -16,6 +16,20 @@ class Config(object): DEBUG = bool(str_to_bool(os.getenv("FLASK_DEBUG", "false"))) TESTING = False APPLICATION_DIR = os.path.abspath(os.path.join(__file__, "..", os.pardir)) + # Base directory where the SQLite database will be stored + + # SQLite database URL + SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(APPLICATION_DIR, 'ttnn.db')}" + + # Enable connection pooling (default for SQLite is disabled) + SQLALCHEMY_ENGINE_OPTIONS = { + "pool_size": 10, # Adjust pool size as needed (default is 5) + "max_overflow": 20, # Allow overflow of the pool size if necessary + "pool_timeout": 30, # Timeout in seconds before giving up on getting a connection + } + + # Disable modification tracking to improve performance + SQLALCHEMY_TRACK_MODIFICATIONS = False class DevelopmentConfig(Config): From 974433df17e93201e8b685b6022cbad6d2eecd15 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 9 Oct 2024 14:59:01 -0300 Subject: [PATCH 15/67] Initialize session and app database --- backend/ttnn_visualizer/extensions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/ttnn_visualizer/extensions.py b/backend/ttnn_visualizer/extensions.py index b84518b1..f7a203bb 100644 --- a/backend/ttnn_visualizer/extensions.py +++ b/backend/ttnn_visualizer/extensions.py @@ -1,4 +1,12 @@ from flask_static_digest import FlaskStaticDigest +from flask_sqlalchemy import SQLAlchemy +from flask_session import Session flask_static_digest = FlaskStaticDigest() +# Initialize Flask SQLAlchemy +db = SQLAlchemy() + + +# Initialize Flask-Session +session = Session() From 38264fa192608f92e5179a61034cd564671e8779 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 9 Oct 2024 14:59:13 -0300 Subject: [PATCH 16/67] Removes unused database setup --- backend/ttnn_visualizer/database.py | 91 ----------------------------- 1 file changed, 91 deletions(-) diff --git a/backend/ttnn_visualizer/database.py b/backend/ttnn_visualizer/database.py index 06ca8999..e69de29b 100644 --- a/backend/ttnn_visualizer/database.py +++ b/backend/ttnn_visualizer/database.py @@ -1,91 +0,0 @@ -import sqlite3 -from logging import getLogger - -logger = getLogger(__name__) - - -def create_update_database(sqlite_db_path): - """ - Creates or updates database with all tables - :param sqlite_db_path Path to target SQLite database - :return: - """ - sqlite_connection = sqlite3.connect(sqlite_db_path) - logger.info("Creating/updating SQLite database") - cursor = sqlite_connection.cursor() - cursor.execute( - """CREATE TABLE IF NOT EXISTS devices - ( - device_id int, - num_y_cores int, - num_x_cores int, - num_y_compute_cores int, - num_x_compute_cores int, - worker_l1_size int, - l1_num_banks int, - l1_bank_size int, - address_at_first_l1_bank int, - address_at_first_l1_cb_buffer int, - num_banks_per_storage_core int, - num_compute_cores int, - num_storage_cores int, - total_l1_memory int, - total_l1_for_tensors int, - total_l1_for_interleaved_buffers int, - total_l1_for_sharded_buffers int, - cb_limit int - )""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS tensors - (tensor_id int UNIQUE, shape text, dtype text, layout text, memory_config text, device_id int, address int, buffer_type int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS local_tensor_comparison_records - (tensor_id int UNIQUE, golden_tensor_id int, matches bool, desired_pcc bool, actual_pcc float)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS global_tensor_comparison_records - (tensor_id int UNIQUE, golden_tensor_id int, matches bool, desired_pcc bool, actual_pcc float)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS operations - (operation_id int UNIQUE, name text, duration float)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS operation_arguments - (operation_id int, name text, value text)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS stack_traces - (operation_id int, stack_trace text)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS input_tensors - (operation_id int, input_index int, tensor_id int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS output_tensors - (operation_id int, output_index int, tensor_id int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS buffers - (operation_id int, device_id int, address int, max_size_per_bank int, buffer_type int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS buffer_pages - (operation_id int, device_id int, address int, core_y int, core_x int, bank_id int, page_index int, page_address int, page_size int, buffer_type int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS nodes - (operation_id int, unique_id int, node_operation_id int, name text)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS edges - (operation_id int, source_unique_id int, sink_unique_id int, source_output_index int, sink_input_index int, key int)""" - ) - cursor.execute( - """CREATE TABLE IF NOT EXISTS captured_graph - (operation_id int, captured_graph text)""" - ) - sqlite_connection.commit() From a361f233729ebef93b045b9cc511ca515eb43f7e Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 9 Oct 2024 15:00:16 -0300 Subject: [PATCH 17/67] Adds session setup to app.py --- backend/ttnn_visualizer/app.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/backend/ttnn_visualizer/app.py b/backend/ttnn_visualizer/app.py index b2c1146b..f3781081 100644 --- a/backend/ttnn_visualizer/app.py +++ b/backend/ttnn_visualizer/app.py @@ -1,5 +1,4 @@ import logging -import shutil from os import environ from pathlib import Path @@ -10,7 +9,6 @@ from flask_cors import CORS from ttnn_visualizer import settings from dotenv import load_dotenv -from ttnn_visualizer.sessions import init_sessions, CustomRequest, init_session_db def create_app(settings_override=None): @@ -31,7 +29,6 @@ def create_app(settings_override=None): flask_env = environ.get("FLASK_ENV", "development") app = Flask(__name__, static_folder=static_assets_dir, static_url_path="/") - app.request_class = CustomRequest app.config.from_object(getattr(settings, flask_env)) @@ -42,8 +39,6 @@ def create_app(settings_override=None): if settings_override: app.config.update(settings_override) - init_session_db() - middleware(app) app.register_blueprint(api) @@ -61,7 +56,7 @@ def catch_all(path): def extensions(app: flask.Flask): - from ttnn_visualizer.extensions import flask_static_digest + from ttnn_visualizer.extensions import flask_static_digest, db, session """ Register 0 or more extensions (mutates the app passed in). @@ -71,6 +66,17 @@ def extensions(app: flask.Flask): """ flask_static_digest.init_app(app) + db.init_app(app) + + app.config["SESSION_TYPE"] = "sqlalchemy" + app.config["SESSION_SQLALCHEMY"] = db + + session.init_app(app) + + # Create the tables within the application context + with app.app_context(): + db.drop_all() + db.create_all() # For automatically reflecting table data # with app.app_context(): @@ -96,8 +102,6 @@ def middleware(app: flask.Flask): # CORS configuration origins = ["http://localhost:5173", "http://localhost:8000"] - init_sessions(app) - CORS( app, origins=origins, From 2a2eca8a0ee6e19285059d7ffcec806ba018286d Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 9 Oct 2024 15:00:36 -0300 Subject: [PATCH 18/67] Refactors sessions.py to use flask-session --- backend/ttnn_visualizer/sessions.py | 201 ++++++++-------------------- 1 file changed, 54 insertions(+), 147 deletions(-) diff --git a/backend/ttnn_visualizer/sessions.py b/backend/ttnn_visualizer/sessions.py index 0f40ba5f..f4de1095 100644 --- a/backend/ttnn_visualizer/sessions.py +++ b/backend/ttnn_visualizer/sessions.py @@ -1,167 +1,74 @@ -import sqlite3 -import json -import threading +from flask import request, jsonify, session, current_app, Request from pathlib import Path -from typing import TypedDict, cast -from flask import request, jsonify, g, Request - -# Path to the SQLite database for sessions -DATABASE = "sessions.db" - - -# Define a TypedDict for the nested structure of the request data -class ActiveReport(TypedDict, total=False): - name: str - hostname: str +def update_tab_session(tab_id, active_report_data, remote_connection_data=None): + """ + Overwrite the active report for a given tab session or create a new session if one doesn't exist. + Store everything in Flask-Session using tab_id as the key. + """ + # Create or update the active report + active_report = {"name": active_report_data.get("name")} + + # Check if remote connection data is provided and add it to the active report + if remote_connection_data: + remote_connection = { + "name": remote_connection_data.get("name"), + "username": remote_connection_data.get("username"), + "host": remote_connection_data.get("host"), + "port": remote_connection_data.get("port"), + "path": remote_connection_data.get("path"), + } + active_report["remote_connection"] = remote_connection + + # Store the active report in the session using the tab_id as the key + session[tab_id] = {"active_report": active_report} + return jsonify({"message": "Tab session updated with new active report"}), 200 + + +def get_or_create_tab_session( + tab_id, active_report_data=None, remote_connection_data=None +): + """ + Retrieve an existing tab session or create a new one if it doesn't exist. + Uses Flask-Session for session management with tab_id as the key. + Initializes session data as an empty dictionary if it doesn't exist. + """ + # Check if the session exists for the given tab_id + session_data = session.get(tab_id) -class TabSessionData(TypedDict): - active_report: ActiveReport + # If session doesn't exist, initialize it as an empty dictionary + if not session_data: + session[tab_id] = {} # Initialize empty session data + session_data = session.get(tab_id) + # If active_report_data is provided, update the session with the new report + if active_report_data: + update_tab_session(tab_id, active_report_data, remote_connection_data) -# Custom request class with session data -class CustomRequest(Request): - tab_session_data: TabSessionData - tab_id: str - report_path: str + return session.get(tab_id), not bool(session_data) -def get_report_path_from_request(): +def get_tab_session(): """ - Gets the currently active report path from the request - :return: + Middleware to retrieve or create a tab session based on the tab_id. """ - from flask import current_app, request as flask_request - - database_file_name = current_app.config["SQLITE_DB_PATH"] - local_dir = current_app.config["LOCAL_DATA_DIRECTORY"] - remote_dir = current_app.config["REMOTE_DATA_DIRECTORY"] - - # For type hinting - request = cast(CustomRequest, flask_request) - - if hasattr(request, "tab_session_data"): - tab_session_data = request.tab_session_data - active_report = tab_session_data.get("active_report", None) - if active_report: - hostname = active_report.get("hostname", None) - if hostname: - base_dir = Path(remote_dir).joinpath(hostname) - else: - base_dir = local_dir - report_path = Path(base_dir).joinpath(active_report.get("name", "")) - target_path = str(Path(report_path).joinpath(database_file_name)) - request.report_path = target_path or "" - else: - request.report_path = "" - - -# Function to initialize the SQLite database and create the tab_sessions table if it doesn't exist -def init_session_db(): - - with threading.Lock(): - print("Initializing session database") - with sqlite3.connect(DATABASE, timeout=30, isolation_level=None) as conn: - cursor = conn.cursor() - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS tab_sessions ( - tab_id TEXT PRIMARY KEY, - session_data TEXT - ) - """ - ) - conn.commit() - - -# Function to get a database connection from the Flask `g` object -def get_db(): - with threading.Lock(): - db = getattr(g, "_session_database", None) - if db is None: - db = g._database = sqlite3.connect( - DATABASE, timeout=30, isolation_level=None - ) - return db - - -# Function to handle session data retrieval and creation -def handle_tab_session(tab_id): - db = get_db() - cur = db.cursor() - cur.execute("SELECT session_data FROM tab_sessions WHERE tab_id = ?", (tab_id,)) - row = cur.fetchone() - - if row is None: - # No session data found, create a new session for this tab as a dictionary - session_data = {"tab_id": tab_id} - cur.execute( - "INSERT INTO tab_sessions (tab_id, session_data) VALUES (?, ?)", - (tab_id, json.dumps(session_data)), - ) - db.commit() - else: - # Session data exists, load it and convert back to a dictionary - session_data = json.loads( - row[0] - ) # Deserialize the JSON string back to a Python dictionary - - return session_data - - -# Function to update the session data for a given tab_id -def update_tab_session(new_data): tab_id = request.args.get("tabId", None) - if not tab_id: - return - db = get_db() - cur = db.cursor() - - # Retrieve existing session data - cur.execute("SELECT session_data FROM tab_sessions WHERE tab_id = ?", (tab_id,)) - row = cur.fetchone() - - if row: - # Update the session data by merging the existing data with new data - existing_data = json.loads(row[0]) # Deserialize the existing JSON string - existing_data.update(new_data) # Update the dictionary with new data - cur.execute( - "UPDATE tab_sessions SET session_data = ? WHERE tab_id = ?", - (json.dumps(existing_data), tab_id), - ) - else: - # If no existing session, create a new session with the new data - session_data = new_data - cur.execute( - "INSERT INTO tab_sessions (tab_id, session_data) VALUES (?, ?)", - (tab_id, json.dumps(session_data)), - ) - - db.commit() - - -# Middleware to fetch or create per-tab session data and attach it to the `request` object -def get_tab_session(): - tab_id = request.args.get("tabId", None) - if tab_id: - request.tab_id = tab_id - request.tab_session_data = handle_tab_session(tab_id) + current_app.logger.info(f"get_tab_session: Received tab_id: {tab_id}") + if not tab_id: + current_app.logger.error("get_tab_session: No tab_id found") + return jsonify({"error": "tabId is required"}), 400 -# Middleware to close the SQLite database connection after each request -def close_db_connection(exception): - db = getattr(g, "_session_database", None) - if db is not None: - db.close() + active_report, created = get_or_create_tab_session(tab_id) + current_app.logger.info(f"get_tab_session: Session retrieved: {active_report}") # Function to initialize the session logic and middleware def init_sessions(app): - - # Add the middleware to the Flask app + """ + Initializes session middleware and hooks it into Flask. + """ app.before_request(get_tab_session) - app.before_request(get_report_path_from_request) - app.teardown_appcontext(close_db_connection) app.logger.info("Sessions middleware initialized.") From b336366d709adef35ee4c9779054efc3725f6d73 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 9 Oct 2024 15:00:51 -0300 Subject: [PATCH 19/67] Moves report_path resolution to utils --- backend/ttnn_visualizer/utils.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/backend/ttnn_visualizer/utils.py b/backend/ttnn_visualizer/utils.py index 1c383184..9ca0a285 100644 --- a/backend/ttnn_visualizer/utils.py +++ b/backend/ttnn_visualizer/utils.py @@ -1,5 +1,6 @@ import logging from functools import wraps +from pathlib import Path from timeit import default_timer from typing import Callable @@ -21,3 +22,32 @@ def wrapper(*args, **kwargs): return response return wrapper + + +def get_report_path(active_report, current_app): + """ + Gets the report path for the given active_report object. + :param active_report: Dictionary representing the active report. + :return: report_path as a string + """ + database_file_name = current_app.config["SQLITE_DB_PATH"] + local_dir = current_app.config["LOCAL_DATA_DIRECTORY"] + remote_dir = current_app.config["REMOTE_DATA_DIRECTORY"] + + if active_report: + # Check if there's an associated RemoteConnection + remote_connection = active_report.get("remote_connection") + if remote_connection: + # Use the remote directory if a remote connection exists + base_dir = Path(remote_dir).joinpath(remote_connection.get("host")) + else: + # Default to local directory if no remote connection is present + base_dir = local_dir + + # Construct the full report path + report_path = Path(base_dir).joinpath(active_report.get("name")) + target_path = str(Path(report_path).joinpath(database_file_name)) + + return target_path + else: + return "" From 5fb410fed723f0259977dc2a5cad00ac17271e4d Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 9 Oct 2024 15:01:12 -0300 Subject: [PATCH 20/67] Uses new session logic in report_path decorator --- backend/ttnn_visualizer/decorators.py | 31 +++++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/backend/ttnn_visualizer/decorators.py b/backend/ttnn_visualizer/decorators.py index abcdbabb..29a80868 100644 --- a/backend/ttnn_visualizer/decorators.py +++ b/backend/ttnn_visualizer/decorators.py @@ -2,24 +2,38 @@ from flask import request, abort from pathlib import Path +from ttnn_visualizer.sessions import get_or_create_tab_session +from ttnn_visualizer.utils import get_report_path + def with_report_path(func): @wraps(func) def wrapper(*args, **kwargs): - target_report_path = getattr(request, "report_path", None) - if not target_report_path or not Path(target_report_path).exists(): + from flask import current_app + + tab_id = request.args.get("tabId") + + if not tab_id: + abort(404) + + session, created = get_or_create_tab_session(tab_id=tab_id) + active_report = session.get("active_report", None) + + if not active_report: # Raise 404 if report_path is missing or does not exist abort(404) + report_path = get_report_path(active_report, current_app) + if not Path(report_path).exists(): + abort(404) + # Add the report path to the view's arguments - kwargs["report_path"] = target_report_path + kwargs["report_path"] = report_path return func(*args, **kwargs) return wrapper - - def remote_exception_handler(func): def remote_handler(*args, **kwargs): from flask import current_app @@ -27,8 +41,11 @@ def remote_handler(*args, **kwargs): from paramiko.ssh_exception import AuthenticationException from paramiko.ssh_exception import NoValidConnectionsError from paramiko.ssh_exception import SSHException - from ttnn_visualizer.exceptions import RemoteFolderException, NoProjectsException - + from ttnn_visualizer.exceptions import ( + RemoteFolderException, + NoProjectsException, + ) + connection = args[0] try: From 97479e879ceb4d11b26981acfd6c5f008ce8bcb1 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 9 Oct 2024 15:01:58 -0300 Subject: [PATCH 21/67] Uses new session framework for active report --- backend/ttnn_visualizer/views.py | 45 +++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/backend/ttnn_visualizer/views.py b/backend/ttnn_visualizer/views.py index 0db96a18..c963de23 100644 --- a/backend/ttnn_visualizer/views.py +++ b/backend/ttnn_visualizer/views.py @@ -3,6 +3,7 @@ import logging from http import HTTPStatus from pathlib import Path +from typing import cast from flask import Blueprint, Response, current_app, request @@ -10,7 +11,6 @@ from ttnn_visualizer.exceptions import RemoteFolderException from ttnn_visualizer.models import RemoteFolder, RemoteConnection, StatusMessage from ttnn_visualizer.queries import DatabaseQueries -from ttnn_visualizer.sessions import ActiveReport from ttnn_visualizer.serializers import ( serialize_operations, @@ -20,7 +20,10 @@ serialize_operations_buffers, serialize_devices, ) -from ttnn_visualizer.sessions import update_tab_session +from ttnn_visualizer.sessions import ( + update_tab_session, + get_or_create_tab_session, +) from ttnn_visualizer.sftp_operations import ( sync_test_folders, read_remote_file, @@ -255,12 +258,9 @@ def create_upload_files(): destination_file.parent.mkdir(exist_ok=True, parents=True) file.save(destination_file) - # Set Active Report on View - active_report = ActiveReport(hostname=None, name=report_name) - current_app.logger.info( - f"Setting active report for {request.tab_id} - {report_directory.name}/{report_name}" + update_tab_session( + tab_id=request.args.get("tabId"), active_report_data={"name": report_name} ) - update_tab_session({"active_report": active_report}) return StatusMessage(status=HTTPStatus.OK, message="Success.").model_dump() @@ -331,12 +331,16 @@ def use_remote_folder(): # Set Active Report on View remote_path = f"{Path(report_data_directory).name}/{connection.host}/{connection_directory.name}" - current_app.logger.info( - f"Setting active report for {request.tab_id} - {remote_path}" + + tab_id = request.args.get("tabId") + current_app.logger.info(f"Setting active report for {tab_id} - {remote_path}") + + update_tab_session( + tab_id=tab_id, + active_report_data={"name": report_folder}, + remote_connection_data=connection.dict(), ) - active_report = ActiveReport(name=report_folder, hostname=connection.host) - update_tab_session({"active_report": active_report}) return Response(status=HTTPStatus.OK) @@ -348,8 +352,19 @@ def health_check(): @api.route("/reports/active", methods=["GET"]) def get_active_folder(): # Used to gate UI functions if no report is active - if hasattr(request, "tab_session_data"): - active_report = request.tab_session_data.get("active_report", None) - if active_report: - return active_report + + tab_id = request.args.get("tabId", None) + current_app.logger.info(f"TabID: {tab_id}") + if tab_id: + session, created = get_or_create_tab_session( + tab_id=tab_id + ) # Capture both the session and created flag + current_app.logger.info(f"Session: {session}") + if session and session.get("active_report", None): + active_report = session.get("active_report") + return { + "name": active_report.get("name"), + "remote_connection": active_report.get("remote_connection", None), + } + return {"name": None, "host": None} From e7eb8c083492f9241b956933ec2ef06163d2ddc0 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 9 Oct 2024 15:04:15 -0300 Subject: [PATCH 22/67] Changes gitignore to target all databases --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 85c41892..32d24ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,4 @@ build .env -sessions.db +*.db From 491ab168bc2a5c829af4b1e2b5681bcc5e6c20a8 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 9 Oct 2024 15:14:01 -0300 Subject: [PATCH 23/67] Adds optional remote_connection value to active report interface --- src/model/APIData.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/model/APIData.ts b/src/model/APIData.ts index 10bce40d..42900337 100644 --- a/src/model/APIData.ts +++ b/src/model/APIData.ts @@ -3,6 +3,7 @@ // SPDX-FileCopyrightText: © 2024 Tenstorrent AI ULC import { Operation, Tensor } from './Graph'; +import { RemoteConnection } from '../definitions/RemoteConnection'; export interface TensorData extends Tensor { shape: string; @@ -41,7 +42,7 @@ export interface OperationDetailsData extends Operation { export interface ActiveReport { name: string; - hostname: string; + remote_connection?: RemoteConnection, } // TODO: we may want to revisit the 'default' portion for the variable name From 355e2cf74a107c5a00a22ac96aeb85a34c9c3dc4 Mon Sep 17 00:00:00 2001 From: David Blundell Date: Wed, 9 Oct 2024 14:19:44 -0400 Subject: [PATCH 24/67] Restored memoryPadding prop --- src/components/BufferSummaryPlotRenderer.tsx | 1 + src/components/BufferSummaryRow.tsx | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/BufferSummaryPlotRenderer.tsx b/src/components/BufferSummaryPlotRenderer.tsx index a2ae02ba..75a392d1 100644 --- a/src/components/BufferSummaryPlotRenderer.tsx +++ b/src/components/BufferSummaryPlotRenderer.tsx @@ -144,6 +144,7 @@ function BufferSummaryPlotRenderer() { operationId={operation.id} memorySize={isZoomedIn ? zoomedMemorySizeEnd + memoryPadding : memorySize} memoryStart={isZoomedIn ? zoomedMemorySizeStart + memoryPadding : 0} + memoryPadding={memoryPadding} />

{operation.id}

diff --git a/src/components/BufferSummaryRow.tsx b/src/components/BufferSummaryRow.tsx index ed4bc4f0..6345cbcf 100644 --- a/src/components/BufferSummaryRow.tsx +++ b/src/components/BufferSummaryRow.tsx @@ -11,6 +11,7 @@ interface BufferSummaryRowProps { operationId: number; memorySize: number; memoryStart?: number; + memoryPadding?: number; } interface Buffer { @@ -22,14 +23,21 @@ interface Buffer { const SCALE = 100; -function BufferSummaryRow({ buffers, operationId, memorySize, memoryStart = 0 }: BufferSummaryRowProps) { +function BufferSummaryRow({ + buffers, + operationId, + memorySize, + memoryStart = 0, + memoryPadding = 0, +}: BufferSummaryRowProps) { const computedMemorySize = memorySize - memoryStart; + const paddingOffset = (memoryPadding / 2 / computedMemorySize) * SCALE; return (
{buffers.map((buffer: Buffer) => { const size = (buffer.size / computedMemorySize) * SCALE; - const position = ((buffer.address - memoryStart) / computedMemorySize) * SCALE; + const position = ((buffer.address - memoryStart) / computedMemorySize) * SCALE + paddingOffset; return (
); })} From ac4e8dab4de4e46d92c3b914be5ef3359707651c Mon Sep 17 00:00:00 2001 From: David Blundell Date: Wed, 9 Oct 2024 14:48:28 -0400 Subject: [PATCH 25/67] Added new progress overlay --- src/components/FileStatusOverlay.tsx | 42 ++++++++++++++++++++++ src/components/Overlay.tsx | 32 ++++++++++++----- src/components/ProgressBar.tsx | 12 ++++--- src/routes/Styleguide.tsx | 18 ++++++++++ src/scss/components/FileStatusOverlay.scss | 4 +++ src/scss/components/ProgressBar.scss | 2 +- 6 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 src/components/FileStatusOverlay.tsx create mode 100644 src/scss/components/FileStatusOverlay.scss diff --git a/src/components/FileStatusOverlay.tsx b/src/components/FileStatusOverlay.tsx new file mode 100644 index 00000000..9f86d6cf --- /dev/null +++ b/src/components/FileStatusOverlay.tsx @@ -0,0 +1,42 @@ +import Overlay from './Overlay'; +import ProgressBar from './ProgressBar'; +import 'styles/components/FileStatusOverlay.scss'; + +interface FileStatusOverlayProps { + isOpen: boolean; + onClose: () => void; +} + +const FILE_DOWNLOAD_STATUS = { + currentFileName: 'foo.tar.gz', + numberOfFiles: 12, + percentOfCurrent: 49, + finishedFiles: 6, +}; + +function FileStatusOverlay({ isOpen, onClose }: FileStatusOverlayProps) { + return ( + +
+

+ Downloading {FILE_DOWNLOAD_STATUS.currentFileName} +

+ +

{`File ${FILE_DOWNLOAD_STATUS.numberOfFiles - FILE_DOWNLOAD_STATUS.finishedFiles} of ${FILE_DOWNLOAD_STATUS.numberOfFiles}`}

+
+ + +
+ ); +} + +export default FileStatusOverlay; diff --git a/src/components/Overlay.tsx b/src/components/Overlay.tsx index e35d42c3..51f28b60 100644 --- a/src/components/Overlay.tsx +++ b/src/components/Overlay.tsx @@ -7,26 +7,40 @@ interface OverlayProps { isOpen: boolean; onClose: () => void; children: ReactNode; + hideCloseButton?: boolean; + canEscapeKeyClose?: boolean; + canOutsideClickClose?: boolean; } -function Overlay({ isOpen, onClose, children }: OverlayProps) { +function Overlay({ + isOpen, + onClose, + children, + hideCloseButton = false, + canEscapeKeyClose = true, + canOutsideClickClose = true, +}: OverlayProps) { return (
{children} -
- -
+ {!hideCloseButton && ( +
+ +
+ )}
); diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx index 306eabdc..1f983e6d 100644 --- a/src/components/ProgressBar.tsx +++ b/src/components/ProgressBar.tsx @@ -1,10 +1,16 @@ import { ProgressBar as BlueprintProgressBar } from '@blueprintjs/core'; import 'styles/components/ProgressBar.scss'; -import { UploadProgress } from '../hooks/useLocal'; -function ProgressBar({ progress, estimated }: UploadProgress) { +interface ProgressBarProps { + progress?: number; + estimated?: number; +} + +function ProgressBar({ progress, estimated }: ProgressBarProps) { return (
+ + {progress && estimated ? ( {progress > 0 ? `${Math.round(progress * 100)}%` : `100%`} @@ -12,8 +18,6 @@ function ProgressBar({ progress, estimated }: UploadProgress) { {estimated > 0 ? `${Math.round(estimated)}s left` : '0s left'} ) : null} - -
); } diff --git a/src/routes/Styleguide.tsx b/src/routes/Styleguide.tsx index 85e2733f..68f17c09 100644 --- a/src/routes/Styleguide.tsx +++ b/src/routes/Styleguide.tsx @@ -13,6 +13,7 @@ import { Switch, Tooltip, } from '@blueprintjs/core'; +import { useState } from 'react'; import { IconNames } from '@blueprintjs/icons'; import { Helmet } from 'react-helmet-async'; import ConnectionTestMessage from '../components/report-selection/ConnectionTestMessage'; @@ -21,6 +22,7 @@ import ProgressBar from '../components/ProgressBar'; import SearchField from '../components/SearchField'; import 'styles/routes/Styleguide.scss'; import LoadingSpinner from '../components/LoadingSpinner'; +import FileStatusOverlay from '../components/FileStatusOverlay'; const FORM_GROUP = { label: 'Form label', @@ -28,6 +30,8 @@ const FORM_GROUP = { }; export default function Operations() { + const [showProgressOverlay, setShowProgressOverlay] = useState(false); + return ( <> @@ -406,6 +410,20 @@ export default function Operations() { />
+
+ + + setShowProgressOverlay(false)} + /> +
+

Connection message

diff --git a/src/scss/components/FileStatusOverlay.scss b/src/scss/components/FileStatusOverlay.scss new file mode 100644 index 00000000..51bfe85b --- /dev/null +++ b/src/scss/components/FileStatusOverlay.scss @@ -0,0 +1,4 @@ +.flex { + display: flex; + justify-content: space-between; +} diff --git a/src/scss/components/ProgressBar.scss b/src/scss/components/ProgressBar.scss index 3dcf959e..94df08e1 100644 --- a/src/scss/components/ProgressBar.scss +++ b/src/scss/components/ProgressBar.scss @@ -4,6 +4,6 @@ width: 100%; .status { - margin-bottom: 5px; + margin-top: 10px; } } From 9ad4e0cbe406841a43666452a5f27edef233a127 Mon Sep 17 00:00:00 2001 From: David Blundell Date: Wed, 9 Oct 2024 15:08:49 -0400 Subject: [PATCH 26/67] Added new overlay to local file upload --- src/components/FileStatusOverlay.tsx | 29 ++++++++++--------- src/components/Overlay.tsx | 2 +- .../report-selection/LocalFolderSelector.tsx | 14 ++++++--- src/hooks/useLocal.tsx | 4 ++- src/model/APIData.ts | 2 +- src/routes/Styleguide.tsx | 11 ++++++- 6 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/components/FileStatusOverlay.tsx b/src/components/FileStatusOverlay.tsx index 9f86d6cf..07244be8 100644 --- a/src/components/FileStatusOverlay.tsx +++ b/src/components/FileStatusOverlay.tsx @@ -4,36 +4,37 @@ import 'styles/components/FileStatusOverlay.scss'; interface FileStatusOverlayProps { isOpen: boolean; - onClose: () => void; + onClose?: () => void; + fileStatus: { + currentFileName: string; + numberOfFiles: number; + percentOfCurrent: number; + finishedFiles: number; + estimatedDuration?: number; + }; + canEscapeKeyClose?: boolean; } -const FILE_DOWNLOAD_STATUS = { - currentFileName: 'foo.tar.gz', - numberOfFiles: 12, - percentOfCurrent: 49, - finishedFiles: 6, -}; - -function FileStatusOverlay({ isOpen, onClose }: FileStatusOverlayProps) { +function FileStatusOverlay({ isOpen, onClose, fileStatus, canEscapeKeyClose = false }: FileStatusOverlayProps) { return (

- Downloading {FILE_DOWNLOAD_STATUS.currentFileName} + Downloading {fileStatus.currentFileName}

-

{`File ${FILE_DOWNLOAD_STATUS.numberOfFiles - FILE_DOWNLOAD_STATUS.finishedFiles} of ${FILE_DOWNLOAD_STATUS.numberOfFiles}`}

+

{`File ${fileStatus.numberOfFiles - fileStatus.finishedFiles} of ${fileStatus.numberOfFiles}`}

); diff --git a/src/components/Overlay.tsx b/src/components/Overlay.tsx index 51f28b60..a28bddf8 100644 --- a/src/components/Overlay.tsx +++ b/src/components/Overlay.tsx @@ -5,7 +5,7 @@ import 'styles/components/Overlay.scss'; interface OverlayProps { isOpen: boolean; - onClose: () => void; + onClose?: () => void; children: ReactNode; hideCloseButton?: boolean; canEscapeKeyClose?: boolean; diff --git a/src/components/report-selection/LocalFolderSelector.tsx b/src/components/report-selection/LocalFolderSelector.tsx index bea9409d..4c1ad0a1 100644 --- a/src/components/report-selection/LocalFolderSelector.tsx +++ b/src/components/report-selection/LocalFolderSelector.tsx @@ -14,7 +14,7 @@ import ROUTES from '../../definitions/routes'; import useLocalConnection from '../../hooks/useLocal'; import { reportLocationAtom } from '../../store/app'; import { ConnectionStatus, ConnectionTestStates } from '../../definitions/ConnectionStatus'; -import ProgressBar from '../ProgressBar'; +import FileStatusOverlay from '../FileStatusOverlay'; const ICON_MAP: Record = { [ConnectionTestStates.IDLE]: IconNames.DOT, @@ -142,9 +142,15 @@ const LocalFolderOptions: FC = () => { {isUploading && uploadProgress?.progress && uploadProgress?.estimated ? ( - ) : null} diff --git a/src/hooks/useLocal.tsx b/src/hooks/useLocal.tsx index ee3cfa90..981e7f4d 100644 --- a/src/hooks/useLocal.tsx +++ b/src/hooks/useLocal.tsx @@ -26,7 +26,9 @@ const useLocalConnection = () => { }, onUploadProgress(uploadStatus) { setUploadProgress({ - progress: uploadStatus.progress, + // uploadStatus.total could be zero with certain requests, but it's not a problem at the moment for us + // https://github.com/axios/axios/issues/1591 + progress: (uploadStatus.loaded * 100) / uploadStatus.total!, estimated: uploadStatus.estimated, }); }, diff --git a/src/model/APIData.ts b/src/model/APIData.ts index 42900337..792116a4 100644 --- a/src/model/APIData.ts +++ b/src/model/APIData.ts @@ -42,7 +42,7 @@ export interface OperationDetailsData extends Operation { export interface ActiveReport { name: string; - remote_connection?: RemoteConnection, + remote_connection?: RemoteConnection; } // TODO: we may want to revisit the 'default' portion for the variable name diff --git a/src/routes/Styleguide.tsx b/src/routes/Styleguide.tsx index 68f17c09..4d16027c 100644 --- a/src/routes/Styleguide.tsx +++ b/src/routes/Styleguide.tsx @@ -29,6 +29,13 @@ const FORM_GROUP = { subLabel: 'Sub label here', }; +const FILE_DOWNLOAD_STATUS = { + currentFileName: 'foo.tar.gz', + numberOfFiles: 12, + percentOfCurrent: 49, + finishedFiles: 6, +}; + export default function Operations() { const [showProgressOverlay, setShowProgressOverlay] = useState(false); @@ -415,12 +422,14 @@ export default function Operations() { onClick={() => setShowProgressOverlay(true)} intent={Intent.PRIMARY} > - Overlay + File status overlay setShowProgressOverlay(false)} + fileStatus={FILE_DOWNLOAD_STATUS} + canEscapeKeyClose />
From 9d0700104246ab8faec800e7b85689f536d90f43 Mon Sep 17 00:00:00 2001 From: David Blundell Date: Wed, 9 Oct 2024 15:11:42 -0400 Subject: [PATCH 27/67] Added licensing text and fixed incorrect braces --- package.json | 2 +- src/components/FileStatusOverlay.tsx | 6 +++++- src/scss/components/FileStatusOverlay.scss | 6 +++++- src/scss/components/ProgressBar.scss | 4 ++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7b718acc..55b05bdd 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ }, "lint-staged": { "**/*.{ts,tsx}": ["eslint ./src --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --no-ignore"], - "**/*.{scss}": "npx stylelint --fix", + "**/*.scss": "npx stylelint --fix", "**/*.{js,jsx,ts,tsx,css,scss}": "npm run format" } diff --git a/src/components/FileStatusOverlay.tsx b/src/components/FileStatusOverlay.tsx index 07244be8..0905e91a 100644 --- a/src/components/FileStatusOverlay.tsx +++ b/src/components/FileStatusOverlay.tsx @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: © 2024 Tenstorrent Inc. + import Overlay from './Overlay'; import ProgressBar from './ProgressBar'; import 'styles/components/FileStatusOverlay.scss'; @@ -24,7 +28,7 @@ function FileStatusOverlay({ isOpen, onClose, fileStatus, canEscapeKeyClose = fa canEscapeKeyClose={canEscapeKeyClose} canOutsideClickClose={false} > -
+

Downloading {fileStatus.currentFileName}

diff --git a/src/scss/components/FileStatusOverlay.scss b/src/scss/components/FileStatusOverlay.scss index 51bfe85b..f12122fb 100644 --- a/src/scss/components/FileStatusOverlay.scss +++ b/src/scss/components/FileStatusOverlay.scss @@ -1,4 +1,8 @@ -.flex { +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: © 2024 Tenstorrent Inc. + +.heading { display: flex; justify-content: space-between; } diff --git a/src/scss/components/ProgressBar.scss b/src/scss/components/ProgressBar.scss index 94df08e1..7cc5b2bf 100644 --- a/src/scss/components/ProgressBar.scss +++ b/src/scss/components/ProgressBar.scss @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: © 2024 Tenstorrent Inc. + .progress-bar { display: flex; flex-direction: column; From 5e28bc182a8e18b7797aacef88833cf2ca98ea8e Mon Sep 17 00:00:00 2001 From: David Blundell Date: Wed, 9 Oct 2024 16:36:00 -0400 Subject: [PATCH 28/67] Padding calculation updated --- src/components/BufferSummaryRow.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/BufferSummaryRow.tsx b/src/components/BufferSummaryRow.tsx index 6345cbcf..efd4e394 100644 --- a/src/components/BufferSummaryRow.tsx +++ b/src/components/BufferSummaryRow.tsx @@ -30,14 +30,14 @@ function BufferSummaryRow({ memoryStart = 0, memoryPadding = 0, }: BufferSummaryRowProps) { - const computedMemorySize = memorySize - memoryStart; - const paddingOffset = (memoryPadding / 2 / computedMemorySize) * SCALE; + const computedMemorySize = memorySize - memoryStart + memoryPadding * 2; + const positionOffset = memoryStart - memoryPadding * 2; return (
{buffers.map((buffer: Buffer) => { const size = (buffer.size / computedMemorySize) * SCALE; - const position = ((buffer.address - memoryStart) / computedMemorySize) * SCALE + paddingOffset; + const position = ((buffer.address - positionOffset) / computedMemorySize) * SCALE; return (
Date: Wed, 9 Oct 2024 16:39:15 -0400 Subject: [PATCH 29/67] Removed optionalness from props --- src/components/BufferSummaryRow.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/components/BufferSummaryRow.tsx b/src/components/BufferSummaryRow.tsx index efd4e394..a4802b9f 100644 --- a/src/components/BufferSummaryRow.tsx +++ b/src/components/BufferSummaryRow.tsx @@ -10,8 +10,8 @@ interface BufferSummaryRowProps { buffers: Buffer[]; operationId: number; memorySize: number; - memoryStart?: number; - memoryPadding?: number; + memoryStart: number; + memoryPadding: number; } interface Buffer { @@ -23,13 +23,7 @@ interface Buffer { const SCALE = 100; -function BufferSummaryRow({ - buffers, - operationId, - memorySize, - memoryStart = 0, - memoryPadding = 0, -}: BufferSummaryRowProps) { +function BufferSummaryRow({ buffers, operationId, memorySize, memoryStart, memoryPadding }: BufferSummaryRowProps) { const computedMemorySize = memorySize - memoryStart + memoryPadding * 2; const positionOffset = memoryStart - memoryPadding * 2; From 89c49b19d8ea7e95823ff275f4facc8d7ad05bea Mon Sep 17 00:00:00 2001 From: David Blundell Date: Thu, 10 Oct 2024 11:42:50 -0400 Subject: [PATCH 30/67] Minor refactoring --- src/components/BufferSummaryPlotRenderer.tsx | 23 +++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/BufferSummaryPlotRenderer.tsx b/src/components/BufferSummaryPlotRenderer.tsx index 75a392d1..2396d827 100644 --- a/src/components/BufferSummaryPlotRenderer.tsx +++ b/src/components/BufferSummaryPlotRenderer.tsx @@ -35,14 +35,21 @@ function BufferSummaryPlotRenderer() { const memorySize = !isLoadingDevices && devices ? devices[0].worker_l1_size : 0; const zoomedMemorySize = useMemo(() => { - const sortedBufferSizes = buffersByOperation - ?.map((operation) => operation.buffers.map((b) => ({ ...b, operationId: operation.id }))) - .flat() - .sort((a, b) => b.address - a.address); - - return sortedBufferSizes && sortedBufferSizes.length > 1 - ? [sortedBufferSizes.at(-1)?.address, sortedBufferSizes[0].address + sortedBufferSizes[0].size] - : [0, memorySize]; + let firstBuffer: undefined | number; + let lastBuffer: undefined | number; + + buffersByOperation?.forEach((operation) => + operation.buffers.forEach((buffer) => { + firstBuffer = + firstBuffer && !Number.isNaN(firstBuffer) ? Math.min(firstBuffer, buffer.address) : buffer.address; + lastBuffer = + lastBuffer && !Number.isNaN(lastBuffer) + ? Math.max(lastBuffer, buffer.address + buffer.size) + : buffer.address + buffer.size; + }), + ); + + return firstBuffer && lastBuffer ? [firstBuffer, lastBuffer] : [0, memorySize]; }, [buffersByOperation, memorySize]); const zoomedMemorySizeStart = zoomedMemorySize[0] || 0; From d7f0a675503b603109bf7d8e3765674f18048e41 Mon Sep 17 00:00:00 2001 From: David Blundell Date: Thu, 10 Oct 2024 14:33:33 -0400 Subject: [PATCH 31/67] Refactored memoryPadding usage --- src/components/BufferSummaryPlotRenderer.tsx | 21 ++++++++++---------- src/components/BufferSummaryRow.tsx | 17 ++++++++++------ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/components/BufferSummaryPlotRenderer.tsx b/src/components/BufferSummaryPlotRenderer.tsx index 2396d827..b49ed859 100644 --- a/src/components/BufferSummaryPlotRenderer.tsx +++ b/src/components/BufferSummaryPlotRenderer.tsx @@ -35,21 +35,20 @@ function BufferSummaryPlotRenderer() { const memorySize = !isLoadingDevices && devices ? devices[0].worker_l1_size : 0; const zoomedMemorySize = useMemo(() => { - let firstBuffer: undefined | number; - let lastBuffer: undefined | number; + let minValue: undefined | number; + let maxValue: undefined | number; buffersByOperation?.forEach((operation) => operation.buffers.forEach((buffer) => { - firstBuffer = - firstBuffer && !Number.isNaN(firstBuffer) ? Math.min(firstBuffer, buffer.address) : buffer.address; - lastBuffer = - lastBuffer && !Number.isNaN(lastBuffer) - ? Math.max(lastBuffer, buffer.address + buffer.size) + minValue = minValue && !Number.isNaN(minValue) ? Math.min(minValue, buffer.address) : buffer.address; + maxValue = + maxValue && !Number.isNaN(maxValue) + ? Math.max(maxValue, buffer.address + buffer.size) : buffer.address + buffer.size; }), ); - return firstBuffer && lastBuffer ? [firstBuffer, lastBuffer] : [0, memorySize]; + return minValue && maxValue ? [minValue, maxValue] : [0, memorySize]; }, [buffersByOperation, memorySize]); const zoomedMemorySizeStart = zoomedMemorySize[0] || 0; @@ -108,7 +107,7 @@ function BufferSummaryPlotRenderer() { memorySize={isZoomedIn ? zoomedMemorySizeEnd : memorySize} plotZoomRange={ isZoomedIn - ? [zoomedMemorySizeStart + memoryPadding, zoomedMemorySizeEnd + memoryPadding] + ? [zoomedMemorySizeStart - memoryPadding, zoomedMemorySizeEnd + memoryPadding] : [0, memorySize] } configuration={BufferSummaryAxisConfiguration} @@ -149,8 +148,8 @@ function BufferSummaryPlotRenderer() { diff --git a/src/components/BufferSummaryRow.tsx b/src/components/BufferSummaryRow.tsx index a4802b9f..2d7c07f5 100644 --- a/src/components/BufferSummaryRow.tsx +++ b/src/components/BufferSummaryRow.tsx @@ -9,8 +9,8 @@ import { getBufferColor } from '../functions/colorGenerator'; interface BufferSummaryRowProps { buffers: Buffer[]; operationId: number; - memorySize: number; memoryStart: number; + memoryEnd: number; memoryPadding: number; } @@ -23,15 +23,20 @@ interface Buffer { const SCALE = 100; -function BufferSummaryRow({ buffers, operationId, memorySize, memoryStart, memoryPadding }: BufferSummaryRowProps) { - const computedMemorySize = memorySize - memoryStart + memoryPadding * 2; - const positionOffset = memoryStart - memoryPadding * 2; +function BufferSummaryRow({ buffers, operationId, memoryStart, memoryEnd, memoryPadding }: BufferSummaryRowProps) { + const computedMemorySize = memoryEnd - memoryStart; + const computedPadding = (memoryPadding / computedMemorySize) * SCALE; return ( -
+
0 ? `0 ${computedPadding}%` : '0', + }} + > {buffers.map((buffer: Buffer) => { const size = (buffer.size / computedMemorySize) * SCALE; - const position = ((buffer.address - positionOffset) / computedMemorySize) * SCALE; + const position = ((buffer.address - memoryStart) / computedMemorySize) * SCALE; return (
Date: Thu, 10 Oct 2024 15:41:55 -0400 Subject: [PATCH 32/67] Removed unused component and moved buffer summary components in their own folder --- .../BufferSummaryPlotRenderer.tsx | 12 ++++++------ .../{ => buffer-summary}/BufferSummaryRow.tsx | 6 ++---- .../buffer-summary/BufferSummaryTable.tsx | 13 +++++++++++++ src/routes/BufferSummary.tsx | 13 ++++--------- 4 files changed, 25 insertions(+), 19 deletions(-) rename src/components/{ => buffer-summary}/BufferSummaryPlotRenderer.tsx (95%) rename src/components/{ => buffer-summary}/BufferSummaryRow.tsx (88%) create mode 100644 src/components/buffer-summary/BufferSummaryTable.tsx diff --git a/src/components/BufferSummaryPlotRenderer.tsx b/src/components/buffer-summary/BufferSummaryPlotRenderer.tsx similarity index 95% rename from src/components/BufferSummaryPlotRenderer.tsx rename to src/components/buffer-summary/BufferSummaryPlotRenderer.tsx index b49ed859..dce7bd0b 100644 --- a/src/components/BufferSummaryPlotRenderer.tsx +++ b/src/components/buffer-summary/BufferSummaryPlotRenderer.tsx @@ -6,13 +6,13 @@ import { UIEvent, useMemo, useRef, useState } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import classNames from 'classnames'; import { Switch } from '@blueprintjs/core'; -import { BufferSummaryAxisConfiguration } from '../definitions/PlotConfigurations'; -import { BuffersByOperationData, useBuffers, useDevices, useOperationsList } from '../hooks/useAPI'; -import { BufferType } from '../model/BufferType'; -import MemoryPlotRenderer from './operation-details/MemoryPlotRenderer'; -import LoadingSpinner from './LoadingSpinner'; +import { BufferSummaryAxisConfiguration } from '../../definitions/PlotConfigurations'; +import { BuffersByOperationData, useBuffers, useDevices, useOperationsList } from '../../hooks/useAPI'; +import { BufferType } from '../../model/BufferType'; +import MemoryPlotRenderer from '../operation-details/MemoryPlotRenderer'; +import LoadingSpinner from '../LoadingSpinner'; import BufferSummaryRow from './BufferSummaryRow'; -import { HistoricalTensor, Operation, Tensor } from '../model/Graph'; +import { HistoricalTensor, Operation, Tensor } from '../../model/Graph'; const PLACEHOLDER_ARRAY_SIZE = 30; const OPERATION_EL_HEIGHT = 20; // Height in px of each list item diff --git a/src/components/BufferSummaryRow.tsx b/src/components/buffer-summary/BufferSummaryRow.tsx similarity index 88% rename from src/components/BufferSummaryRow.tsx rename to src/components/buffer-summary/BufferSummaryRow.tsx index 2d7c07f5..c664df7d 100644 --- a/src/components/BufferSummaryRow.tsx +++ b/src/components/buffer-summary/BufferSummaryRow.tsx @@ -2,9 +2,9 @@ // // SPDX-FileCopyrightText: © 2024 Tenstorrent Inc. -import { BufferType } from '../model/BufferType'; +import { BufferType } from '../../model/BufferType'; import 'styles/components/BufferSummaryRow.scss'; -import { getBufferColor } from '../functions/colorGenerator'; +import { getBufferColor } from '../../functions/colorGenerator'; interface BufferSummaryRowProps { buffers: Buffer[]; @@ -47,8 +47,6 @@ function BufferSummaryRow({ buffers, operationId, memoryStart, memoryEnd, memory left: `${position}%`, backgroundColor: getBufferColor(buffer.address), }} - data-address={buffer.address} - data-size={buffer.size} /> ); })} diff --git a/src/components/buffer-summary/BufferSummaryTable.tsx b/src/components/buffer-summary/BufferSummaryTable.tsx new file mode 100644 index 00000000..50f563ec --- /dev/null +++ b/src/components/buffer-summary/BufferSummaryTable.tsx @@ -0,0 +1,13 @@ +function BufferSummaryTable() { + return ( + + + + + + +
BufferSummaryTable goes here
+ ); +} + +export default BufferSummaryTable; diff --git a/src/routes/BufferSummary.tsx b/src/routes/BufferSummary.tsx index 01204662..43b8ac91 100644 --- a/src/routes/BufferSummary.tsx +++ b/src/routes/BufferSummary.tsx @@ -8,7 +8,8 @@ import { useEffect } from 'react'; import { useReportMeta } from '../hooks/useAPI'; import { reportMetaAtom } from '../store/app'; import 'styles/components/BufferSummaryPlot.scss'; -import BufferSummaryPlotRenderer from '../components/BufferSummaryPlotRenderer'; +import BufferSummaryPlotRenderer from '../components/buffer-summary/BufferSummaryPlotRenderer'; +import BufferSummaryTable from '../components/buffer-summary/BufferSummaryTable'; export default function BufferSummary() { const report = useReportMeta(); @@ -26,14 +27,8 @@ export default function BufferSummary() { - {/* - - - - - - -
BufferSummaryTable goes here
*/} + + ); } From fd26ec5660b1497b3bcc1ec6e355912fae8b0aad Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Thu, 10 Oct 2024 21:26:44 -0300 Subject: [PATCH 33/67] Triggers save on session changes --- backend/ttnn_visualizer/sessions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/ttnn_visualizer/sessions.py b/backend/ttnn_visualizer/sessions.py index f4de1095..0f288036 100644 --- a/backend/ttnn_visualizer/sessions.py +++ b/backend/ttnn_visualizer/sessions.py @@ -23,6 +23,8 @@ def update_tab_session(tab_id, active_report_data, remote_connection_data=None): # Store the active report in the session using the tab_id as the key session[tab_id] = {"active_report": active_report} + + session.modified = True return jsonify({"message": "Tab session updated with new active report"}), 200 @@ -46,6 +48,8 @@ def get_or_create_tab_session( if active_report_data: update_tab_session(tab_id, active_report_data, remote_connection_data) + session.modified = True + return session.get(tab_id), not bool(session_data) From 032ff1b6c1879289dcb2355e46821c58beacdde9 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Thu, 10 Oct 2024 21:28:19 -0300 Subject: [PATCH 34/67] Change samesite / HTTPs settings for cookies --- backend/ttnn_visualizer/sessions.py | 4 ++-- backend/ttnn_visualizer/settings.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/ttnn_visualizer/sessions.py b/backend/ttnn_visualizer/sessions.py index 0f288036..44cc3ca4 100644 --- a/backend/ttnn_visualizer/sessions.py +++ b/backend/ttnn_visualizer/sessions.py @@ -42,13 +42,13 @@ def get_or_create_tab_session( # If session doesn't exist, initialize it as an empty dictionary if not session_data: session[tab_id] = {} # Initialize empty session data + session.modified = True session_data = session.get(tab_id) # If active_report_data is provided, update the session with the new report if active_report_data: update_tab_session(tab_id, active_report_data, remote_connection_data) - - session.modified = True + session.modified = True return session.get(tab_id), not bool(session_data) diff --git a/backend/ttnn_visualizer/settings.py b/backend/ttnn_visualizer/settings.py index 677ede9e..a32403c4 100644 --- a/backend/ttnn_visualizer/settings.py +++ b/backend/ttnn_visualizer/settings.py @@ -30,6 +30,8 @@ class Config(object): # Disable modification tracking to improve performance SQLALCHEMY_TRACK_MODIFICATIONS = False + SESSION_COOKIE_SAMESITE = "Lax" + SESSION_COOKIE_SECURE = False # For development on HTTP class DevelopmentConfig(Config): From 7953f228e9df42bdbf37b98bc07e019e0cbfc909 Mon Sep 17 00:00:00 2001 From: David Blundell Date: Fri, 11 Oct 2024 11:48:33 -0400 Subject: [PATCH 35/67] Add fancy blueprint table --- .../buffer-summary/BufferSummaryTable.tsx | 96 +++++++++++++++++-- src/scss/components/BufferSummaryTable.scss | 3 + 2 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 src/scss/components/BufferSummaryTable.scss diff --git a/src/components/buffer-summary/BufferSummaryTable.tsx b/src/components/buffer-summary/BufferSummaryTable.tsx index 50f563ec..e9980f5d 100644 --- a/src/components/buffer-summary/BufferSummaryTable.tsx +++ b/src/components/buffer-summary/BufferSummaryTable.tsx @@ -1,12 +1,92 @@ +import { HotkeysProvider } from '@blueprintjs/core'; +import { Table2 as BlueprintTable, Cell, Column } from '@blueprintjs/table'; +import { useBuffers, useOperationsList } from '../../hooks/useAPI'; +import { BufferType, BufferTypeLabel } from '../../model/BufferType'; +import LoadingSpinner from '../LoadingSpinner'; +import '@blueprintjs/table/lib/css/table.css'; +import 'styles/components/BufferSummaryTable.scss'; + +const HEADING_LABELS = ['', 'Operation Id', 'Operation Name', 'Address', 'Size', 'Buffer Type', 'Device Id']; +const HEADINGS = { + 0: '', + 1: 'operationId', + 2: 'operationName', + 3: 'address', + 4: 'size', + 5: 'buffer_type', + 6: 'device_id', +}; + +interface Buffer { + address: number; + buffer_type: number; + device_id: number; + size: number; + operationId: number; + operationName: string; +} + function BufferSummaryTable() { - return ( - - - - - - -
BufferSummaryTable goes here
+ const { data: operations } = useOperationsList(); + const { data: buffersByOperation, isLoading: isLoadingBuffers } = useBuffers(BufferType.L1); + + let listOfBuffers: Buffer[] = []; + + if (buffersByOperation && operations) { + listOfBuffers = buffersByOperation + .map((operation) => + operation.buffers + .map((buffer) => ({ + ...buffer, + operationId: operation.id, + operationName: operations.find((op) => op.id === operation.id)?.name, + })) + .flat(), + ) + .flat() as Buffer[]; + } + + const createColumns = () => { + return HEADING_LABELS.map((heading, index) => createColumn(heading, index)); + }; + + const createColumn = (heading: string, colIndex: number) => { + return ( + + ); + }; + + // eslint-disable-next-line react/no-unstable-nested-components + const createCell = (colIndex: keyof typeof HEADINGS) => (rowIndex: number) => { + const cellContent = getCellContent(colIndex, rowIndex); + + return {cellContent}; + }; + + const getCellContent = (colIndex: keyof typeof HEADINGS, rowIndex: number) => { + if (HEADINGS[colIndex] === 'buffer_type') { + return BufferTypeLabel[listOfBuffers[rowIndex][HEADINGS[colIndex]]]; + } + + return colIndex === 0 ? rowIndex + 1 : listOfBuffers[rowIndex][HEADINGS[colIndex]]; + }; + + return !isLoadingBuffers && buffersByOperation ? ( + + + {createColumns()} + + + ) : ( + ); } diff --git a/src/scss/components/BufferSummaryTable.scss b/src/scss/components/BufferSummaryTable.scss new file mode 100644 index 00000000..a3b15078 --- /dev/null +++ b/src/scss/components/BufferSummaryTable.scss @@ -0,0 +1,3 @@ +.buffer-summary-table { + height: 600px; +} From 53c6dfb60a6a08476c64a6f8520be89520ab503a Mon Sep 17 00:00:00 2001 From: David Blundell Date: Tue, 15 Oct 2024 11:06:59 -0400 Subject: [PATCH 36/67] Cleaned up getting cell content and changed table to inline flex --- .../buffer-summary/BufferSummaryTable.tsx | 24 ++++++++++--------- src/model/BufferType.ts | 2 +- src/scss/components/BufferSummaryTable.scss | 1 + 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/components/buffer-summary/BufferSummaryTable.tsx b/src/components/buffer-summary/BufferSummaryTable.tsx index e9980f5d..53eede26 100644 --- a/src/components/buffer-summary/BufferSummaryTable.tsx +++ b/src/components/buffer-summary/BufferSummaryTable.tsx @@ -6,15 +6,14 @@ import LoadingSpinner from '../LoadingSpinner'; import '@blueprintjs/table/lib/css/table.css'; import 'styles/components/BufferSummaryTable.scss'; -const HEADING_LABELS = ['', 'Operation Id', 'Operation Name', 'Address', 'Size', 'Buffer Type', 'Device Id']; +const HEADING_LABELS = ['Operation Id', 'Operation Name', 'Address', 'Size', 'Buffer Type', 'Device Id']; const HEADINGS = { - 0: '', - 1: 'operationId', - 2: 'operationName', - 3: 'address', - 4: 'size', - 5: 'buffer_type', - 6: 'device_id', + 0: 'operationId', + 1: 'operationName', + 2: 'address', + 3: 'size', + 4: 'buffer_type', + 5: 'device_id', }; interface Buffer { @@ -68,11 +67,14 @@ function BufferSummaryTable() { }; const getCellContent = (colIndex: keyof typeof HEADINGS, rowIndex: number) => { - if (HEADINGS[colIndex] === 'buffer_type') { - return BufferTypeLabel[listOfBuffers[rowIndex][HEADINGS[colIndex]]]; + const cellBuffer = listOfBuffers[rowIndex]; + const cellHeading = HEADINGS[colIndex] as keyof Buffer; + + if (cellHeading === 'buffer_type') { + return BufferTypeLabel[cellBuffer.buffer_type]; } - return colIndex === 0 ? rowIndex + 1 : listOfBuffers[rowIndex][HEADINGS[colIndex]]; + return cellBuffer[cellHeading]; }; return !isLoadingBuffers && buffersByOperation ? ( diff --git a/src/model/BufferType.ts b/src/model/BufferType.ts index 812ed84f..b7324b8d 100644 --- a/src/model/BufferType.ts +++ b/src/model/BufferType.ts @@ -13,7 +13,7 @@ export enum BufferType { TRACE, } -export const BufferTypeLabel: Record = { +export const BufferTypeLabel: Record = { [BufferType.DRAM]: 'DRAM', [BufferType.L1]: 'L1', [BufferType.SYSTEM_MEMORY]: 'System Memory', diff --git a/src/scss/components/BufferSummaryTable.scss b/src/scss/components/BufferSummaryTable.scss index a3b15078..da3968c1 100644 --- a/src/scss/components/BufferSummaryTable.scss +++ b/src/scss/components/BufferSummaryTable.scss @@ -1,3 +1,4 @@ .buffer-summary-table { + display: inline-flex; height: 600px; } From 25b3c99fd8a12daac87e99627c79c6807bce9e06 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 12:20:13 -0300 Subject: [PATCH 37/67] Adds handlers for socket operations - Adds socketio handlers for associating socket sessions with tab IDs - Adds messages for file transfer progress and compression progress - Socket messages are broadcast to associated tab - Socket file messages are debounced (compression messages can be sent quite rapidly) --- backend/ttnn_visualizer/sockets.py | 128 +++++++++++++++++++++++++++++ src/main.tsx | 10 ++- 2 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 backend/ttnn_visualizer/sockets.py diff --git a/backend/ttnn_visualizer/sockets.py b/backend/ttnn_visualizer/sockets.py new file mode 100644 index 00000000..234810c1 --- /dev/null +++ b/backend/ttnn_visualizer/sockets.py @@ -0,0 +1,128 @@ +import dataclasses +import threading +import time +from dataclasses import dataclass +from datetime import datetime +from enum import Enum +from logging import getLogger + +from flask_socketio import join_room, disconnect, leave_room + +logger = getLogger(__name__) + + +class Messages(object): + FILE_TRANSFER_PROGRESS = "fileTransferProgress" + + +class FileStatus(Enum): + DOWNLOADING = "DOWNLOADING" + FAILED = "FAILED" + COMPRESSING = "COMPRESSING" + FINISHED = "FINISHED" + STARTED = "STARTED" + + +@dataclass +class FileProgress: + current_file_name: str + number_of_files: int + percent_of_current: float + finished_files: int + status: FileStatus # Use the FileStatus Enum + timestamp: str = datetime.utcnow().isoformat() + + def __post_init__(self): + self.status = FileStatus(self.status).value if self.status is not None else None + self.percent_of_current = round(self.percent_of_current, 2) + + +# For tracking connected clients subscriber ID +tab_clients = {} + +# Global variables for debouncing +debounce_timer = None +debounce_delay = 0.5 # Delay in seconds (adjust as needed) + + +def emit_file_status(progress: FileProgress, tab_id=None): + """Debounced emit for file status updates using a debounce timer.""" + global debounce_timer, last_emit_time + + def emit_now(): + global last_emit_time + last_emit_time = time.time() + data = dataclasses.asdict(progress) + data.update({"tab_id": tab_id}) + socketio.emit(Messages.FILE_TRANSFER_PROGRESS, data, to=tab_id) + + # Cancel any existing debounce timer if it exists and is still active + if debounce_timer and isinstance(debounce_timer, threading.Timer): + debounce_timer.cancel() + + # Check if the last emit was longer than debounce_delay + if time.time() - last_emit_time > debounce_delay: + emit_now() + else: + # Set a new debounce timer + debounce_timer = threading.Timer(debounce_delay, emit_now) + debounce_timer.start() + + +def emit_compression_progress(client, remote_tar_path, folder_size, sid): + """Emit progress during the compression.""" + compressed_size = 0 + while compressed_size < folder_size: + stdin, stdout, stderr = client.exec_command(f"du -sb {remote_tar_path}") + compressed_size_str = stdout.read().decode().strip().split("\t")[0] + compressed_size = int(compressed_size_str) + percent_of_compression = (compressed_size / folder_size) * 100 + print(percent_of_compression) + progress = FileProgress( + current_file_name=remote_tar_path, + number_of_files=1, + percent_of_current=percent_of_compression, + finished_files=0, + status=FileStatus.COMPRESSING.value, + ) + emit_file_status(progress, tab_id=sid) + + +def register_handlers(socketio_instance): + global socketio + socketio = socketio_instance + + @socketio.on("connect") + def handle_connect(): + from flask import request + + tab_id = request.args.get("tabId") + print( + f"Received tabId: {tab_id}, socket ID: {request.sid}" + ) # Log for debugging + + if tab_id: + join_room(tab_id) # Join the room identified by the tabId + tab_clients[tab_id] = ( + request.sid + ) # Store the socket ID associated with this tabId + print(f"Joined room: {tab_id}") + else: + print("No tabId provided, disconnecting client.") + disconnect() + + @socketio.on("disconnect") + def handle_disconnect(): + from flask import request + + tab_id = None + # Find and remove the socket ID associated with this tabId + for key, value in tab_clients.items(): + + if value == request.sid: + tab_id = key + break + if tab_id: + leave_room(tab_id) + del tab_clients[tab_id] + print(f"Client disconnected from tabId: {tab_id}, Socket ID: {request.sid}") diff --git a/src/main.tsx b/src/main.tsx index ebd1400b..902cba90 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -18,6 +18,7 @@ import Styleguide from './routes/Styleguide'; import ROUTES from './definitions/routes'; import Tensors from './routes/Tensors'; import BufferSummary from './routes/BufferSummary'; +import { SocketProvider } from './libs/SocketProvider'; const router = createBrowserRouter([ { @@ -67,9 +68,12 @@ ReactDOM.createRoot(document.getElementById('root')!).render( - - - + + + + + + , From 97312b99fa36d446bd477647557fde7bfad699bc Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 12:20:40 -0300 Subject: [PATCH 38/67] Adds eventlet and socketio dependencies --- backend/ttnn_visualizer/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/ttnn_visualizer/requirements.txt b/backend/ttnn_visualizer/requirements.txt index 5dffabd4..ffef2bae 100644 --- a/backend/ttnn_visualizer/requirements.txt +++ b/backend/ttnn_visualizer/requirements.txt @@ -9,6 +9,8 @@ flask_static_digest==0.4.1 setuptools==65.5.0 python-dotenv==1.0.1 flask-sqlalchemy +flask-socketio +eventlet flask-session wheel build From cff0a29de61160e96875fc2c00376ca05b8f8f32 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 12:21:32 -0300 Subject: [PATCH 39/67] Adds threading configuration to settings.py - Changes settings.py to use a singleton pattern to facilitate importing it through application --- backend/ttnn_visualizer/settings.py | 65 ++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/backend/ttnn_visualizer/settings.py b/backend/ttnn_visualizer/settings.py index a32403c4..4af1f810 100644 --- a/backend/ttnn_visualizer/settings.py +++ b/backend/ttnn_visualizer/settings.py @@ -4,50 +4,77 @@ from ttnn_visualizer.utils import str_to_bool -class Config(object): - TEST_CONFIG_FILE = "config.json" +class DefaultConfig(object): + # General Settings + SECRET_KEY = os.getenv("SECRET_KEY", "90909") + DEBUG = bool(str_to_bool(os.getenv("FLASK_DEBUG", "false"))) + TESTING = False + + # Path Settings REPORT_DATA_DIRECTORY = Path(__file__).parent.absolute().joinpath("data") LOCAL_DATA_DIRECTORY = Path(REPORT_DATA_DIRECTORY).joinpath("local") REMOTE_DATA_DIRECTORY = Path(REPORT_DATA_DIRECTORY).joinpath("remote") + APPLICATION_DIR = os.path.abspath(os.path.join(__file__, "..", os.pardir)) SEND_FILE_MAX_AGE_DEFAULT = 0 - MIGRATE_ON_COPY = True + + # File Name Configs + TEST_CONFIG_FILE = "config.json" SQLITE_DB_PATH = "db.sqlite" - SECRET_KEY = os.getenv("SECRET_KEY", "90909") - DEBUG = bool(str_to_bool(os.getenv("FLASK_DEBUG", "false"))) - TESTING = False - APPLICATION_DIR = os.path.abspath(os.path.join(__file__, "..", os.pardir)) - # Base directory where the SQLite database will be stored - # SQLite database URL + # SQL Alchemy Settings SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(APPLICATION_DIR, 'ttnn.db')}" - - # Enable connection pooling (default for SQLite is disabled) SQLALCHEMY_ENGINE_OPTIONS = { "pool_size": 10, # Adjust pool size as needed (default is 5) "max_overflow": 20, # Allow overflow of the pool size if necessary "pool_timeout": 30, # Timeout in seconds before giving up on getting a connection } - - # Disable modification tracking to improve performance SQLALCHEMY_TRACK_MODIFICATIONS = False + + # Gunicorn settings + GUNICORN_WORKER_CLASS = os.getenv("GUNICORN_WORKER_CLASS", "gevent") + GUNICORN_WORKERS = os.getenv("GUNICORN_WORKERS", "1") + GUNICORN_BIND = os.getenv("GUNICORN_BIND", "localhost:8000") + GUNICORN_APP_MODULE = os.getenv( + "GUNICORN_APP_MODULE", "ttnn_visualizer.app:create_app()" + ) + + # Session Settings SESSION_COOKIE_SAMESITE = "Lax" SESSION_COOKIE_SECURE = False # For development on HTTP -class DevelopmentConfig(Config): +class DevelopmentConfig(DefaultConfig): pass -class TestingConfig(Config): +class TestingConfig(DefaultConfig): DEBUG = bool(str_to_bool(os.getenv("FLASK_DEBUG", "True"))) TESTING = True -class ProductionConfig(Config): +class ProductionConfig(DefaultConfig): DEBUG = False TESTING = False -development = DevelopmentConfig() -testing = TestingConfig() -production = ProductionConfig() +class Config: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(Config, cls).__new__(cls) + cls._instance = cls._determine_config() + return cls._instance + + @staticmethod + def _determine_config(): + # Determine the environment + flask_env = os.getenv("FLASK_ENV", "development").lower() + + # Choose the correct configuration class based on FLASK_ENV + if flask_env == "production": + return ProductionConfig() + elif flask_env == "testing": + return TestingConfig() + else: + return DevelopmentConfig() From 1f595847e5a9f903054b55400b173d49bdd14b44 Mon Sep 17 00:00:00 2001 From: David Blundell Date: Tue, 15 Oct 2024 11:25:39 -0400 Subject: [PATCH 40/67] Added table package --- package-lock.json | 61 ++++++++++++++++++++++++++++++++++------------- package.json | 8 ++++--- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index b173d5d1..da9d1de2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "ttnn-visualzer", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ttnn-visualzer", - "version": "0.4.0", + "version": "0.5.0", "dependencies": { "@blueprintjs/core": "^5.10.3", "@blueprintjs/select": "^5.1.5", + "@blueprintjs/table": "^5.2.2", "@tanstack/react-virtual": "^3.5.1", "@types/tinycolor2": "^1.4.6", "axios": "^1.7.2", @@ -637,20 +638,20 @@ "peer": true }, "node_modules/@blueprintjs/colors": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@blueprintjs/colors/-/colors-5.1.1.tgz", - "integrity": "sha512-Mni/GgCYLaMf5U5zsCN42skOG49w3U0QmUFuJgFZ/1pv+3cHF/9xR4L4dXcj5DtgJoHkmUbK36PR5mdFB65WEA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@blueprintjs/colors/-/colors-5.1.2.tgz", + "integrity": "sha512-7CWwVsXK4YTN9Z/wkjnS3p7VE8YfIXXv2UaySAbtcw6rBkmoSHjLRtfohSA5yNy8xYTQ4KY2odKZSUW0W/Nltw==", "dependencies": { "tslib": "~2.6.2" } }, "node_modules/@blueprintjs/core": { - "version": "5.10.3", - "resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-5.10.3.tgz", - "integrity": "sha512-QlfvAkZOimeyJKs9+EKd52nYfHd1yltSJQLAmv0+0wF/5m+eQ5jeWjF8Mr/vwq2EqiC3pS2+i3mivORvxXTgDg==", + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-5.13.1.tgz", + "integrity": "sha512-PDZ9X/xGBetwU2AqQuCDGVWZZMmt6+/BCnmoKXxsBBZIuWQuHDiKcwvb9rzup0htsA6P7KGl5aw7ocmDvZPpBw==", "dependencies": { - "@blueprintjs/colors": "^5.1.1", - "@blueprintjs/icons": "^5.9.0", + "@blueprintjs/colors": "^5.1.2", + "@blueprintjs/icons": "^5.13.0", "@popperjs/core": "^2.11.8", "classnames": "^2.3.1", "normalize.css": "^8.0.1", @@ -676,9 +677,9 @@ } }, "node_modules/@blueprintjs/icons": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-5.10.0.tgz", - "integrity": "sha512-Qp9H2U2/m6+p0Lh9ACuF2O6o0uVINMFimo+EgasgGaLx8SkJsl7ApB3hYQ+iRSYlQWotnoqd4Vtzj4c7sKukHQ==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-5.13.0.tgz", + "integrity": "sha512-L096dBjzfnWW7fWXM311S2C/5Zn0EuEK9q6G84QvWP0BZJOTowU1EIWLj90IgGtNajld/3ZUAj6eJf+ryt/kjQ==", "dependencies": { "change-case": "^4.1.2", "classnames": "^2.3.1", @@ -1013,6 +1014,27 @@ } } }, + "node_modules/@blueprintjs/table": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@blueprintjs/table/-/table-5.2.2.tgz", + "integrity": "sha512-kGaUkn0TyUcIifPQOK5wBRxbrxEwsgJB+aaoDF6gDc5mO9vj1kUiNpeygu5h2mAYjOPmLEk9BknO1ODurt5pwg==", + "dependencies": { + "@blueprintjs/core": "^5.13.1", + "classnames": "^2.3.1", + "react-innertext": "^1.1.5", + "tslib": "~2.6.2" + }, + "peerDependencies": { + "@types/react": "^16.14.41 || 17 || 18", + "react": "^16.8 || 17 || 18", + "react-dom": "^16.8 || 17 || 18" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@choojs/findup": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@choojs/findup/-/findup-0.2.1.tgz", @@ -3313,8 +3335,7 @@ "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "devOptional": true + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/q": { "version": "1.5.8", @@ -3326,7 +3347,6 @@ "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "devOptional": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -13565,6 +13585,15 @@ "react": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-innertext": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz", + "integrity": "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==", + "peerDependencies": { + "@types/react": ">=0.0.0 <=99", + "react": ">=0.0.0 <=99" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", diff --git a/package.json b/package.json index 55b05bdd..58b31231 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@blueprintjs/core": "^5.10.3", "@blueprintjs/select": "^5.1.5", + "@blueprintjs/table": "^5.2.2", "@tanstack/react-virtual": "^3.5.1", "@types/tinycolor2": "^1.4.6", "axios": "^1.7.2", @@ -85,9 +86,10 @@ } }, "lint-staged": { - "**/*.{ts,tsx}": ["eslint ./src --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --no-ignore"], - "**/*.scss": "npx stylelint --fix", + "**/*.{ts,tsx}": [ + "eslint ./src --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --no-ignore" + ], + "**/*.scss": "npx stylelint --fix", "**/*.{js,jsx,ts,tsx,css,scss}": "npm run format" - } } From e2a7c020c1a606d28e289e46f0672b9e28c670e5 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:18:23 -0300 Subject: [PATCH 41/67] Adds additional logging for active report session data --- backend/ttnn_visualizer/decorators.py | 9 ++++++++- backend/ttnn_visualizer/sessions.py | 15 ++++++++++----- backend/ttnn_visualizer/views.py | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/backend/ttnn_visualizer/decorators.py b/backend/ttnn_visualizer/decorators.py index 29a80868..48134f5c 100644 --- a/backend/ttnn_visualizer/decorators.py +++ b/backend/ttnn_visualizer/decorators.py @@ -14,17 +14,24 @@ def wrapper(*args, **kwargs): tab_id = request.args.get("tabId") if not tab_id: + current_app.logger.error("No tabId present on request, returning 404") abort(404) - session, created = get_or_create_tab_session(tab_id=tab_id) + session = get_or_create_tab_session(tab_id=tab_id) active_report = session.get("active_report", None) if not active_report: + current_app.logger.error( + f"No active report exists for tabId {tab_id}, returning 404" + ) # Raise 404 if report_path is missing or does not exist abort(404) report_path = get_report_path(active_report, current_app) if not Path(report_path).exists(): + current_app.logger.error( + f"Specified report path {report_path} does not exist, returning 404" + ) abort(404) # Add the report path to the view's arguments diff --git a/backend/ttnn_visualizer/sessions.py b/backend/ttnn_visualizer/sessions.py index 44cc3ca4..e81dd15f 100644 --- a/backend/ttnn_visualizer/sessions.py +++ b/backend/ttnn_visualizer/sessions.py @@ -1,5 +1,8 @@ -from flask import request, jsonify, session, current_app, Request -from pathlib import Path +from logging import getLogger + +from flask import request, jsonify, session, current_app + +logger = getLogger(__name__) def update_tab_session(tab_id, active_report_data, remote_connection_data=None): @@ -23,8 +26,10 @@ def update_tab_session(tab_id, active_report_data, remote_connection_data=None): # Store the active report in the session using the tab_id as the key session[tab_id] = {"active_report": active_report} - session.modified = True + + current_app.logger.info(f"Set active report for tab {tab_id} to {active_report}") + return jsonify({"message": "Tab session updated with new active report"}), 200 @@ -50,7 +55,7 @@ def get_or_create_tab_session( update_tab_session(tab_id, active_report_data, remote_connection_data) session.modified = True - return session.get(tab_id), not bool(session_data) + return session_data def get_tab_session(): @@ -64,7 +69,7 @@ def get_tab_session(): current_app.logger.error("get_tab_session: No tab_id found") return jsonify({"error": "tabId is required"}), 400 - active_report, created = get_or_create_tab_session(tab_id) + active_report = get_or_create_tab_session(tab_id) current_app.logger.info(f"get_tab_session: Session retrieved: {active_report}") diff --git a/backend/ttnn_visualizer/views.py b/backend/ttnn_visualizer/views.py index 6437a48e..8a7590fb 100644 --- a/backend/ttnn_visualizer/views.py +++ b/backend/ttnn_visualizer/views.py @@ -375,7 +375,7 @@ def get_active_folder(): tab_id = request.args.get("tabId", None) current_app.logger.info(f"TabID: {tab_id}") if tab_id: - session, created = get_or_create_tab_session( + session = get_or_create_tab_session( tab_id=tab_id ) # Capture both the session and created flag current_app.logger.info(f"Session: {session}") From cd43be1e486e06321d86690d8b57c61cc4377ef8 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:20:31 -0300 Subject: [PATCH 42/67] Use singleton for app config in app.py - Updates run command for Flask - Adds socketio to UI dependencies - Uses gunicorn environment variables in run command --- backend/ttnn_visualizer/app.py | 46 ++++++++++++++++++++++------ backend/ttnn_visualizer/appserver.py | 17 ++++++++-- package.json | 10 +++--- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/backend/ttnn_visualizer/app.py b/backend/ttnn_visualizer/app.py index 7d31de05..14ef6b27 100644 --- a/backend/ttnn_visualizer/app.py +++ b/backend/ttnn_visualizer/app.py @@ -1,4 +1,5 @@ import logging +import subprocess from os import environ from pathlib import Path @@ -9,7 +10,7 @@ from werkzeug.debug import DebuggedApplication from werkzeug.middleware.proxy_fix import ProxyFix -from ttnn_visualizer import settings +from ttnn_visualizer.settings import Config def create_app(settings_override=None): @@ -31,7 +32,7 @@ def create_app(settings_override=None): app = Flask(__name__, static_folder=static_assets_dir, static_url_path="/") - app.config.from_object(getattr(settings, flask_env)) + app.config.from_object(Config()) logging.basicConfig(level=app.config.get("LOG_LEVEL", "INFO")) @@ -40,6 +41,7 @@ def create_app(settings_override=None): if settings_override: app.config.update(settings_override) + app.config["USE_WEBSOCKETS"] = True # Set this based on environment middleware(app) app.register_blueprint(api) @@ -57,7 +59,8 @@ def catch_all(path): def extensions(app: flask.Flask): - from ttnn_visualizer.extensions import flask_static_digest, db, session + from ttnn_visualizer.extensions import flask_static_digest, db, session, socketio + from ttnn_visualizer.sockets import register_handlers """ Register 0 or more extensions (mutates the app passed in). @@ -67,16 +70,22 @@ def extensions(app: flask.Flask): """ flask_static_digest.init_app(app) + socketio.init_app(app) db.init_app(app) app.config["SESSION_TYPE"] = "sqlalchemy" app.config["SESSION_SQLALCHEMY"] = db + with app.app_context(): + db.drop_all() + session.init_app(app) + # Register the handlers by passing the socketio instance + + register_handlers(socketio) # Create the tables within the application context with app.app_context(): - db.drop_all() db.create_all() # For automatically reflecting table data @@ -93,12 +102,14 @@ def middleware(app: flask.Flask): :param app: Flask application instance :return: None """ - # Enable the Flask interactive debugger in the broswer for development. - if app.debug: - app.wsgi_app = DebuggedApplication(app.wsgi_app, evalex=True) + # Only use the middleware if running in pure WSGI (HTTP requests) + if not app.config.get("USE_WEBSOCKETS"): + # Enable the Flask interactive debugger in the browser for development. + if app.debug: + app.wsgi_app = DebuggedApplication(app.wsgi_app, evalex=True) - # Set the real IP address into request.remote_addr when behind a proxy. - app.wsgi_app = ProxyFix(app.wsgi_app) + # Set the real IP address into request.remote_addr when behind a proxy. + app.wsgi_app = ProxyFix(app.wsgi_app) # CORS configuration origins = ["http://localhost:5173", "http://localhost:8000"] @@ -109,3 +120,20 @@ def middleware(app: flask.Flask): ) return None + + +if __name__ == "__main__": + config = Config() + + gunicorn_args = [ + "gunicorn", + "-k", + config.GUNICORN_WORKER_CLASS, + "-w", + config.GUNICORN_WORKERS, + config.GUNICORN_APP_MODULE, + "-b", + config.GUNICORN_BIND, + ] + + subprocess.run(gunicorn_args) diff --git a/backend/ttnn_visualizer/appserver.py b/backend/ttnn_visualizer/appserver.py index 629314f2..81f26572 100644 --- a/backend/ttnn_visualizer/appserver.py +++ b/backend/ttnn_visualizer/appserver.py @@ -4,18 +4,29 @@ from gunicorn.app.wsgiapp import run +from ttnn_visualizer.settings import Config + app_dir = pathlib.Path(__file__).parent.resolve() config_dir = app_dir.joinpath("config") static_assets_dir = app_dir.joinpath("static") def serve(): + """Run command for use in wheel package entrypoint""" + config = Config() + os.environ.setdefault("FLASK_ENV", "production") os.environ.setdefault("STATIC_ASSETS", str(static_assets_dir)) + sys.argv = [ "gunicorn", - "-c", - str(config_dir.joinpath("gunicorn.py").absolute()), - "ttnn_visualizer.app:create_app()", + "-k", + config.GUNICORN_WORKER_CLASS, + "-w", + config.GUNICORN_WORKERS, + "-b", + config.GUNICORN_BIND, + config.GUNICORN_APP_MODULE, ] + run() diff --git a/package.json b/package.json index 55b05bdd..13633252 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,scss,css,json,md}\"", - "flask:start": "PYTHONPATH=backend flask --app ttnn_visualizer.app run -h localhost -p 8000", + "flask:start": "PYTHONPATH=backend python -m ttnn_visualizer.app", "flask:test": "PYTHONPATH=backend pytest backend/ttnn_visualizer/tests", "flask:start-threaded": "PYTHONPATH=backend flask --app ttnn_visualizer.app run --with-threads -h localhost -p 8000", "flask:start-debug": "PYTHONPATH=backend flask --app ttnn_visualizer.app --debug run -h localhost -p 8000", @@ -36,6 +36,7 @@ "react-router": "^6.23.1", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", + "socket.io-client": "^4.8.0", "tinycolor2": "^1.6.0" }, "devDependencies": { @@ -85,9 +86,10 @@ } }, "lint-staged": { - "**/*.{ts,tsx}": ["eslint ./src --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --no-ignore"], - "**/*.scss": "npx stylelint --fix", + "**/*.{ts,tsx}": [ + "eslint ./src --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --no-ignore" + ], + "**/*.scss": "npx stylelint --fix", "**/*.{js,jsx,ts,tsx,css,scss}": "npm run format" - } } From c5e4ceff76cde264cd2294265398aa8c66769218 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:21:15 -0300 Subject: [PATCH 43/67] Adds socketio extension to extensions.py --- backend/ttnn_visualizer/extensions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/ttnn_visualizer/extensions.py b/backend/ttnn_visualizer/extensions.py index f7a203bb..48c98f29 100644 --- a/backend/ttnn_visualizer/extensions.py +++ b/backend/ttnn_visualizer/extensions.py @@ -1,3 +1,4 @@ +from flask_socketio import SocketIO from flask_static_digest import FlaskStaticDigest from flask_sqlalchemy import SQLAlchemy from flask_session import Session @@ -10,3 +11,5 @@ # Initialize Flask-Session session = Session() + +socketio = SocketIO(cors_allowed_origins="*", async_mode="gevent") From 1a771efe0c7be72349788d77d8c97a6c9bfe2c20 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:21:46 -0300 Subject: [PATCH 44/67] Adds background threading to SFTP operations and local file upload view --- backend/ttnn_visualizer/sftp_operations.py | 274 +++++++++++++-------- backend/ttnn_visualizer/views.py | 70 +++++- 2 files changed, 235 insertions(+), 109 deletions(-) diff --git a/backend/ttnn_visualizer/sftp_operations.py b/backend/ttnn_visualizer/sftp_operations.py index 2f39526c..ec9d96f6 100644 --- a/backend/ttnn_visualizer/sftp_operations.py +++ b/backend/ttnn_visualizer/sftp_operations.py @@ -1,13 +1,22 @@ import json +import logging import os -from pathlib import Path import tarfile +from pathlib import Path from stat import S_ISDIR from typing import List +from flask import current_app from paramiko.client import SSHClient from paramiko.sftp_client import SFTPClient -import logging + +from ttnn_visualizer.extensions import socketio +from ttnn_visualizer.sockets import ( + FileProgress, + FileStatus, + emit_compression_progress, + emit_file_status, +) from ttnn_visualizer.decorators import remote_exception_handler from ttnn_visualizer.exceptions import NoProjectsException from ttnn_visualizer.models import RemoteConnection, RemoteFolder @@ -19,8 +28,130 @@ REPORT_DATA_DIRECTORY = Path(__file__).parent.absolute().joinpath("data") +def start_background_task(task, *args): + with current_app.app_context(): + """Start a background task with SocketIO.""" + socketio.start_background_task(task, *args) + + +def calculate_folder_size(client: SSHClient, folder_path: str) -> int: + """Calculate the total size of the folder before compression.""" + stdin, stdout, stderr = client.exec_command(f"du -sb {folder_path}") + size_info = stdout.read().decode().strip().split("\t")[0] + return int(size_info) + + +def read_remote_config(sftp: SFTPClient, config_path: str) -> RemoteFolder: + """Read a remote config file and return RemoteFolder object.""" + attributes = sftp.lstat(str(config_path)) + with sftp.open(str(config_path), "rb") as config_file: + data = json.loads(config_file.read()) + return RemoteFolder( + remotePath=str(Path(config_path).parent), + testName=data["report_name"], + lastModified=attributes.st_mtime, + ) + + +def walk_sftp_directory(sftp: SFTPClient, remote_path: str): + """SFTP implementation of os.walk.""" + files, folders = [], [] + for f in sftp.listdir_attr(remote_path): + if S_ISDIR(f.st_mode): + folders.append(f.filename) + else: + files.append(f.filename) + return files, folders + + +@remote_exception_handler +def sync_files_and_directories( + client, remote_folder: RemoteFolder, destination_dir: Path, sid=None +): + """Download files and directories sequentially in one unified loop.""" + with client.open_sftp() as sftp: + # Ensure the destination directory exists + destination_dir.mkdir(parents=True, exist_ok=True) + finished_files = 0 # Initialize finished files counter + + # Recursively handle files and folders in the current directory + def download_directory_contents(remote_dir, local_dir): + # Ensure the local directory exists + local_dir.mkdir(parents=True, exist_ok=True) + + # Get files and folders in the remote directory + files, folders = walk_sftp_directory(sftp, remote_dir) + total_files = len(files) + + # Function to download a file with progress reporting + def download_file(remote_file_path, local_file_path, index): + nonlocal finished_files + # Download file with progress callback + download_file_with_progress( + sftp, + remote_file_path, + local_file_path, + sid, + total_files, + finished_files, + ) + finished_files += 1 + + # Download all files in the current directory + for index, file in enumerate(files, start=1): + remote_file_path = f"{remote_dir}/{file}" + local_file_path = Path(local_dir, file) + download_file(remote_file_path, local_file_path, index) + + # Recursively handle subdirectories + for folder in folders: + remote_subdir = f"{remote_dir}/{folder}" + local_subdir = local_dir / folder + download_directory_contents(remote_subdir, local_subdir) + + # Start downloading from the root folder + download_directory_contents(remote_folder.remotePath, destination_dir) + + # Emit final status + final_progress = FileProgress( + current_file_name="", # No specific file for the final status + number_of_files=0, + percent_of_current=100, + finished_files=finished_files, + status=FileStatus.FINISHED.value, + ) + emit_file_status(final_progress, sid) + logger.info("All files downloaded. Final progress emitted.") + + +def download_file_with_progress( + sftp, remote_path, local_path, sid, total_files, finished_files +): + """Download a file and emit progress using FileProgress.""" + try: + + def download_progress_callback(transferred, total): + percent_of_current = (transferred / total) * 100 + progress = FileProgress( + current_file_name=remote_path, + number_of_files=total_files, + percent_of_current=percent_of_current, + finished_files=finished_files, + status=FileStatus.DOWNLOADING, + ) + emit_file_status(progress, sid) + + # Perform the download + sftp.get(remote_path, str(local_path), callback=download_progress_callback) + + except OSError as e: + logger.error(f"Error downloading file {remote_path} to {local_path}: {str(e)}") + raise + + @remote_exception_handler def read_remote_file(remote_connection): + """Read a remote file.""" logger.info(f"Opening remote file {remote_connection.path}") ssh_client = get_client(remote_connection) with ssh_client.open_sftp() as sftp: @@ -33,14 +164,16 @@ def read_remote_file(remote_connection): return content +@remote_exception_handler +def check_remote_path(remote_connection): + """Check the remote path for config files.""" + ssh_client = get_client(remote_connection) + get_remote_folder_config_paths(remote_connection, ssh_client) + + @remote_exception_handler def get_remote_folder_config_paths(remote_connection, ssh_client) -> List[str]: - """ - Given a remote path return a list of report config files - :param ssh_client: - :param remote_connection - :return: - """ + """Given a remote path, return a list of report config files.""" remote_path = remote_connection.path project_configs = [] with ssh_client.open_sftp() as sftp: @@ -62,12 +195,7 @@ def get_remote_folder_config_paths(remote_connection, ssh_client) -> List[str]: def get_remote_folders( ssh_client: SSHClient, remote_configs: List[str] ) -> List[RemoteFolder]: - """ - Given a list of remote config paths return a list of RemoteFolder objects - :param ssh_client: - :param remote_configs: - :return: - """ + """Return a list of RemoteFolder objects.""" remote_folder_data = [] with ssh_client.open_sftp() as sftp: for config in remote_configs: @@ -77,12 +205,7 @@ def get_remote_folders( @remote_exception_handler def get_remote_test_folders(remote_connection: RemoteConnection) -> List[RemoteFolder]: - """ - Return a list of remote folders given a remote connection - Checks for directories containing a config.json file - :param remote_connection: - :return: - """ + """Return a list of remote folders containing a config.json file.""" client = get_client(remote_connection) remote_config_paths = get_remote_folder_config_paths(remote_connection, client) if not remote_config_paths: @@ -92,90 +215,37 @@ def get_remote_test_folders(remote_connection: RemoteConnection) -> List[RemoteF return get_remote_folders(client, remote_config_paths) -def walk_sftp_directory(sftp, remote_path): - """ - SFTP implementation of os.walk - :param sftp: Connected SFTP client - :param remote_path: - :return: - """ - files = [] - folders = [] - for f in sftp.listdir_attr(remote_path): - if S_ISDIR(f.st_mode): - folders.append(f.filename) - else: - files.append(f.filename) - return files, folders - - -@remote_exception_handler -def check_remote_path(remote_connection): - ssh_client = get_client(remote_connection) - get_remote_folder_config_paths(remote_connection, ssh_client) - - -def read_remote_config(sftp: SFTPClient, config_path: str): - attributes = sftp.lstat(str(config_path)) - with sftp.open(str(config_path), "rb") as config_file: - data = json.loads(config_file.read()) - return RemoteFolder( - remotePath=str(Path(config_path).parent), - testName=data["report_name"], - lastModified=attributes.st_mtime, - ) - - -@remote_exception_handler -def sync_files_individually(client, remote_folder: RemoteFolder, destination_dir): - with client.open_sftp() as sftp: - destination_dir.mkdir(parents=True, exist_ok=True) - files, folders = walk_sftp_directory(sftp, remote_folder.remotePath) - - for file in files: - remote_file_path = f"{remote_folder.remotePath}/{file}" - local_file_path = Path(destination_dir, file) - - logger.info(f"Downloading file: {remote_file_path} to {local_file_path}") - sftp.get(remote_file_path, str(local_file_path)) - - for folder in folders: - remote_subdir = f"{remote_folder.remotePath}/{folder}" - local_subdir = Path(destination_dir, folder) - local_subdir.mkdir(parents=True, exist_ok=True) - - subdir_files, _ = walk_sftp_directory(sftp, remote_subdir) - for sub_file in subdir_files: - remote_file_path = f"{remote_subdir}/{sub_file}" - local_file_path = Path(local_subdir, sub_file) - logger.info( - f"Downloading file: {remote_file_path} to {local_file_path}" - ) - sftp.get(remote_file_path, str(local_file_path)) - - @remote_exception_handler def sync_test_folders( - remote_connection: RemoteConnection, remote_folder: RemoteFolder, path_prefix: str + remote_connection: RemoteConnection, + remote_folder: RemoteFolder, + path_prefix: str, + sid=None, ): + """Main function to sync test folders, handles both compressed and individual syncs.""" client = get_client(remote_connection) - - gzip_exists = check_gzip_exists(client) - report_folder = Path(remote_folder.remotePath).name destination_dir = Path( - REPORT_DATA_DIRECTORY, - path_prefix, - remote_connection.host, - report_folder, + REPORT_DATA_DIRECTORY, path_prefix, remote_connection.host, report_folder ) destination_dir.mkdir(parents=True, exist_ok=True) check_permissions(client, remote_folder.remotePath) - if gzip_exists: + if not check_gzip_exists(client): try: remote_tar_path = f"{remote_folder.remotePath}.tar.gz" + folder_size = calculate_folder_size(client, remote_folder.remotePath) + + logger.info( + f"Beginning compression of remote folder {remote_folder.remotePath}" + ) + # Emit compression progress in the background + start_background_task( + emit_compression_progress, client, remote_tar_path, folder_size, sid + ) + + # Compress the folder compress_command = ( f"tar -czf {remote_tar_path} -C {remote_folder.remotePath} ." ) @@ -187,18 +257,28 @@ def sync_test_folders( local_tar_path = Path(destination_dir, f"{report_folder}.tar.gz") logger.info(f"Downloading compressed folder: {local_tar_path}") + # Download compressed folder in the background with client.open_sftp() as sftp: - sftp.get(remote_tar_path, str(local_tar_path)) + start_background_task( + download_file_with_progress, + sftp, + remote_tar_path, + local_tar_path, + sid, + 1, + 0, + ) + # Extract tar file with tarfile.open(local_tar_path, "r:gz") as tar: tar.extractall(path=destination_dir) os.remove(local_tar_path) - client.exec_command(f"rm {remote_tar_path}") - except Exception: - logger.error("Compression failed, falling back to individual sync.") - sync_files_individually(client, remote_folder, destination_dir) + + except Exception as e: + logger.error(f"Compression failed: {e}, falling back to individual sync.") + sync_files_and_directories(client, remote_folder, destination_dir, sid) else: logger.info("gzip/tar not found, syncing files individually.") - sync_files_individually(client, remote_folder, destination_dir) + sync_files_and_directories(client, remote_folder, destination_dir, sid) diff --git a/backend/ttnn_visualizer/views.py b/backend/ttnn_visualizer/views.py index 6437a48e..2ceaacc5 100644 --- a/backend/ttnn_visualizer/views.py +++ b/backend/ttnn_visualizer/views.py @@ -1,17 +1,21 @@ import dataclasses import json -import logging -from http import HTTPStatus -from pathlib import Path -from typing import cast -from flask import Blueprint, Response, current_app, request +from flask import Blueprint, Response from ttnn_visualizer.decorators import with_report_path from ttnn_visualizer.exceptions import RemoteFolderException from ttnn_visualizer.models import RemoteFolder, RemoteConnection, StatusMessage from ttnn_visualizer.queries import DatabaseQueries - +from ttnn_visualizer.sockets import ( + FileProgress, + emit_file_status, + FileStatus, +) +from flask import request, current_app +from pathlib import Path +from http import HTTPStatus +import logging from ttnn_visualizer.serializers import ( serialize_operations, serialize_tensors, @@ -251,12 +255,14 @@ def get_devices(report_path): ], ) def create_upload_files(): + """Handle file uploads and emit progress for each file.""" files = request.files.getlist("files") local_dir = current_app.config["LOCAL_DATA_DIRECTORY"] filenames = [Path(f.filename).name for f in files] logger.info(f"Received files: {filenames}") + # Validate necessary files if "db.sqlite" not in filenames or "config.json" not in filenames: return StatusMessage( status=HTTPStatus.INTERNAL_SERVER_ERROR, @@ -266,20 +272,56 @@ def create_upload_files(): report_name = files[0].filename.split("/")[0] report_directory = Path(local_dir) logger.info(f"Writing report files to {report_directory}/{report_name}") - for file in files: - logger.info(f"Processing file: {file.filename}") - destination_file = Path(report_directory, Path(file.filename)) + + total_files = len(files) + processed_files = 0 + tab_id = request.args.get("tabId") + + for index, file in enumerate(files): + current_file_name = file.filename + logger.info(f"Processing file: {current_file_name}") + + destination_file = Path(report_directory, Path(current_file_name)) logger.info(f"Writing file to {destination_file}") + + # Create the directory if it doesn't exist if not destination_file.parent.exists(): logger.info( f"{destination_file.parent.name} does not exist. Creating directory" ) destination_file.parent.mkdir(exist_ok=True, parents=True) + + # Emit 0% progress at the start + progress = FileProgress( + current_file_name=current_file_name, + number_of_files=total_files, + percent_of_current=0, + finished_files=processed_files, + status=FileStatus.DOWNLOADING, + ) + emit_file_status(progress, tab_id) + + # Save the file locally file.save(destination_file) - update_tab_session( - tab_id=request.args.get("tabId"), active_report_data={"name": report_name} + # Emit 100% progress after file is saved + processed_files += 1 + progress.percent_of_current = 100 + progress.finished_files = processed_files + emit_file_status(progress, tab_id) + + # Update the session after all files are uploaded + update_tab_session(tab_id=tab_id, active_report_data={"name": report_name}) + + # Emit final success status after all files are processed + final_progress = FileProgress( + current_file_name=None, + number_of_files=total_files, + percent_of_current=100, + finished_files=processed_files, + status=FileStatus.FINISHED, ) + emit_file_status(final_progress, tab_id) return StatusMessage(status=HTTPStatus.OK, message="Success.").model_dump() @@ -320,9 +362,13 @@ def sync_remote_folder(): request_body = request.json connection = request_body.get("connection") folder = request_body.get("folder") + tab_id = request.args.get("tabId", None) try: sync_test_folders( - RemoteConnection(**connection), RemoteFolder(**folder), remote_dir + RemoteConnection(**connection), + RemoteFolder(**folder), + remote_dir, + sid=tab_id, ) except RemoteFolderException as e: return Response(status=e.status, response=e.message) From 2162beb563b4acaab7ddb35cef08427876909ddd Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:22:07 -0300 Subject: [PATCH 45/67] Export getOrCreateTabId --- src/libs/axiosInstance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/axiosInstance.ts b/src/libs/axiosInstance.ts index 9c00431d..0a6d20cb 100644 --- a/src/libs/axiosInstance.ts +++ b/src/libs/axiosInstance.ts @@ -9,7 +9,7 @@ const axiosInstance = axios.create({ baseURL: '/', // Your API base URL }); -const getOrCreateTabId = () => { +export const getOrCreateTabId = () => { let tabId = sessionStorage.getItem('tab_id'); if (!tabId) { tabId = Math.random().toString(36).substring(2, 15); From 56e586c6efd922ea2a13759241473282c7577053 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:22:49 -0300 Subject: [PATCH 46/67] Adds a socket provider for using sockets within application --- src/libs/SocketProvider.tsx | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/libs/SocketProvider.tsx diff --git a/src/libs/SocketProvider.tsx b/src/libs/SocketProvider.tsx new file mode 100644 index 00000000..a376c4a8 --- /dev/null +++ b/src/libs/SocketProvider.tsx @@ -0,0 +1,55 @@ +import React, { ReactNode, createContext, useEffect } from 'react'; +import { Socket, io } from 'socket.io-client'; +import { getOrCreateTabId } from './axiosInstance'; + +// Define the type for the socket +export type SocketContextType = Socket | null; + +// Initialize the socket connection (replace with your backend URL) +const socket = io(`http://localhost:8000?tabId=${getOrCreateTabId()}`); + +// Create the SocketContext with a default value of `null` +export const SocketContext = createContext(null); + +// TypeScript interface for the provider props +interface SocketProviderProps { + children: ReactNode; // React children components +} + +export const SocketProvider: React.FC = ({ children }) => { + useEffect(() => { + // Debugging: Listen for connection and disconnection events + socket.on('connect', () => { + console.log(`Socket connected with ID: ${socket.id}`); + }); + + socket.on('disconnect', (reason: string) => { + console.log(`Socket disconnected: ${reason}`); + }); + + socket.on('connect_error', (error: Error) => { + console.error(`Socket connection error: ${error.message}`); + }); + + socket.on('reconnect', (attemptNumber: number) => { + console.log(`Socket reconnected after ${attemptNumber} attempts`); + }); + + /* For debugging socket messages */ + // socket.onAny((eventName: string, data: any ) => { + // console.info(`Socket ${eventName}: ${JSON.stringify(data)}`); + // }) + + + return () => { + // Cleanup socket listeners on unmount + // socket.offAny(); + socket.off('connect'); + socket.off('disconnect'); + socket.off('connect_error'); + socket.off('reconnect'); + }; + }, []); + + return {children}; +}; From 07f6f7e9af5e4ffd7c15178ca0bc65718f909dbb Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:23:27 -0300 Subject: [PATCH 47/67] Adds interface for API data for file progress --- src/model/APIData.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/model/APIData.ts b/src/model/APIData.ts index 792116a4..888c7f79 100644 --- a/src/model/APIData.ts +++ b/src/model/APIData.ts @@ -45,6 +45,24 @@ export interface ActiveReport { remote_connection?: RemoteConnection; } +export enum FileStatus { + DOWNLOADING = "DOWNLOADING", + FAILED = "FAILED", + COMPRESSING = "COMPRESSING", + FINISHED = "FINISHED", + STARTED = "STARTED" +} + +// TypeScript Interface with underscored keys to match the backend data +export interface FileProgress { + current_file_name: string; + number_of_files: number; + percent_of_current: number, + finished_files: number; + status: FileStatus; // Use the FileStatus enum + timestamp?: string; // Optional, with default handled elsewhere if necessary +} + // TODO: we may want to revisit the 'default' portion for the variable name export const defaultOperationDetailsData: OperationDetailsData = { id: 0, From cad564b3616ef493a3996fb7ce33ed8371129038 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:23:44 -0300 Subject: [PATCH 48/67] Adds an atom for storing the file progress --- src/store/app.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/store/app.ts b/src/store/app.ts index c6ea4d5e..3d272546 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -4,7 +4,7 @@ import { atomWithStorage } from 'jotai/utils'; import { atom } from 'jotai'; -import { ReportMetaData } from '../model/APIData'; +import { FileStatus, ReportMetaData } from '../model/APIData'; const reportMetaKey = 'reportMeta'; const reportLocationKey = 'reportLocation'; @@ -18,3 +18,13 @@ export const expandedTensorsAtom = atom([]); export const selectedTensorAddressAtom = atom(null); export const isL1ActiveAtom = atom(true); export const isDramActiveAtom = atom(false); + +// This atom stores the file transfer progress data in localStorage (or sessionStorage) +export const fileTransferProgressAtom = atom({ + currentFileName: '', + numberOfFiles: 0, + percentOfCurrent: 0, + finishedFiles: 0, + status: FileStatus.FINISHED + } +); From 26700e92ad69303e77e621567e28a76e7f9c7c31 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:24:20 -0300 Subject: [PATCH 49/67] Adds an overlay component wired to socket for displaying file operation progress --- src/components/FileStatusOverlay.tsx | 78 +++++++++++++++++++++------- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/src/components/FileStatusOverlay.tsx b/src/components/FileStatusOverlay.tsx index 0905e91a..b1602139 100644 --- a/src/components/FileStatusOverlay.tsx +++ b/src/components/FileStatusOverlay.tsx @@ -5,40 +5,80 @@ import Overlay from './Overlay'; import ProgressBar from './ProgressBar'; import 'styles/components/FileStatusOverlay.scss'; +import { fileTransferProgressAtom } from '../store/app'; +import { useAtom } from 'jotai'; +import { useContext, useEffect, useState } from 'react'; +import { SocketContext, SocketContextType } from '../libs/SocketProvider'; +import { getOrCreateTabId } from '../libs/axiosInstance'; +import { FileProgress, FileStatus } from '../model/APIData'; interface FileStatusOverlayProps { - isOpen: boolean; onClose?: () => void; - fileStatus: { - currentFileName: string; - numberOfFiles: number; - percentOfCurrent: number; - finishedFiles: number; - estimatedDuration?: number; - }; canEscapeKeyClose?: boolean; } -function FileStatusOverlay({ isOpen, onClose, fileStatus, canEscapeKeyClose = false }: FileStatusOverlayProps) { +function FileStatusOverlay({ canEscapeKeyClose = false }: FileStatusOverlayProps) { + const [fileTransferProgress, setFileTransferProgress] = useAtom(fileTransferProgressAtom); + const [tabId] = useState(getOrCreateTabId()) + const socket = useContext(SocketContext); + + const formatPercentage = (percentage: number) => percentage.toFixed(2).padStart(5, '0'); + + + + useEffect(() => { + + if (!socket) { + return; + } + + socket.on('connect', () => { + // Reset the progress when the socket reconnects (possible HMR case) + setFileTransferProgress({ + currentFileName: '', + numberOfFiles: 0, + percentOfCurrent: 0, + finishedFiles: 0, + status: FileStatus.FINISHED, + }); + }); + + // Listen for file transfer progress messages + socket.on('fileTransferProgress', (data: FileProgress & { tab_id: string }) => { + if (data.tab_id === tabId) { + setFileTransferProgress({ + currentFileName: data.current_file_name, + numberOfFiles: data.number_of_files, + percentOfCurrent: data.percent_of_current, + finishedFiles: data.finished_files, + status: data.status, + }); + } + }); + + // Clean up the WebSocket connection on component unmount + return () => { + socket.off('fileTransferProgress'); + }; + + }, [socket, setFileTransferProgress]); return ( -
-

- Downloading {fileStatus.currentFileName} -

- -

{`File ${fileStatus.numberOfFiles - fileStatus.finishedFiles} of ${fileStatus.numberOfFiles}`}

+
+

File Transfer Progress

+

Current File: {fileTransferProgress.currentFileName}

+

Files Transferred: {fileTransferProgress.finishedFiles}/{fileTransferProgress.numberOfFiles}

+

Current File Progress: {formatPercentage(fileTransferProgress.percentOfCurrent)}%

+

Status: {fileTransferProgress.status}

); From c63ac684d39f6e7684a84bece4460101792eb7fb Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:26:22 -0300 Subject: [PATCH 50/67] Adds overlay to folder selector - Display of folder selector is controlled by socket connection --- .../report-selection/LocalFolderSelector.tsx | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/components/report-selection/LocalFolderSelector.tsx b/src/components/report-selection/LocalFolderSelector.tsx index 4c1ad0a1..7e4913d9 100644 --- a/src/components/report-selection/LocalFolderSelector.tsx +++ b/src/components/report-selection/LocalFolderSelector.tsx @@ -35,7 +35,7 @@ const LocalFolderOptions: FC = () => { const queryClient = useQueryClient(); const [reportLocation, setReportLocation] = useAtom(reportLocationAtom); - const { uploadLocalFolder, uploadProgress } = useLocalConnection(); + const { uploadLocalFolder } = useLocalConnection(); const [folderStatus, setFolderStatus] = useState(); const [isUploading, setIsUploading] = useState(false); const [localUploadLabel, setLocalUploadLabel] = useState('Choose directory...'); @@ -141,18 +141,8 @@ const LocalFolderOptions: FC = () => { View report - {isUploading && uploadProgress?.progress && uploadProgress?.estimated ? ( - - ) : null} + // TODO This should live higher in the component stack maybe + {folderStatus && !isUploading && (
From 3755be2fd6045dac9202eeb960ae525a65e765d3 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:30:59 -0300 Subject: [PATCH 51/67] Fix linting errors --- src/components/FileStatusOverlay.tsx | 30 +++++++++---------- .../report-selection/LocalFolderSelector.tsx | 4 +-- src/libs/SocketProvider.tsx | 2 +- src/model/APIData.ts | 16 +++++----- src/store/app.ts | 13 ++++---- 5 files changed, 30 insertions(+), 35 deletions(-) diff --git a/src/components/FileStatusOverlay.tsx b/src/components/FileStatusOverlay.tsx index b1602139..5d5995a0 100644 --- a/src/components/FileStatusOverlay.tsx +++ b/src/components/FileStatusOverlay.tsx @@ -2,32 +2,28 @@ // // SPDX-FileCopyrightText: © 2024 Tenstorrent Inc. +import { useAtom } from 'jotai'; +import { useContext, useEffect, useState } from 'react'; import Overlay from './Overlay'; import ProgressBar from './ProgressBar'; import 'styles/components/FileStatusOverlay.scss'; -import { fileTransferProgressAtom } from '../store/app'; -import { useAtom } from 'jotai'; -import { useContext, useEffect, useState } from 'react'; import { SocketContext, SocketContextType } from '../libs/SocketProvider'; import { getOrCreateTabId } from '../libs/axiosInstance'; import { FileProgress, FileStatus } from '../model/APIData'; +import { fileTransferProgressAtom } from '../store/app'; interface FileStatusOverlayProps { - onClose?: () => void; canEscapeKeyClose?: boolean; } function FileStatusOverlay({ canEscapeKeyClose = false }: FileStatusOverlayProps) { const [fileTransferProgress, setFileTransferProgress] = useAtom(fileTransferProgressAtom); - const [tabId] = useState(getOrCreateTabId()) + const [tabId] = useState(getOrCreateTabId()); const socket = useContext(SocketContext); const formatPercentage = (percentage: number) => percentage.toFixed(2).padStart(5, '0'); - - useEffect(() => { - if (!socket) { return; } @@ -57,29 +53,31 @@ function FileStatusOverlay({ canEscapeKeyClose = false }: FileStatusOverlayProps }); // Clean up the WebSocket connection on component unmount + // eslint-disable-next-line consistent-return return () => { socket.off('fileTransferProgress'); }; - - }, [socket, setFileTransferProgress]); + }, [socket, setFileTransferProgress, tabId]); return ( -
+

File Transfer Progress

Current File: {fileTransferProgress.currentFileName}

-

Files Transferred: {fileTransferProgress.finishedFiles}/{fileTransferProgress.numberOfFiles}

+

+ Files Transferred: {fileTransferProgress.finishedFiles}/{fileTransferProgress.numberOfFiles} +

Current File Progress: {formatPercentage(fileTransferProgress.percentOfCurrent)}%

Status: {fileTransferProgress.status}

- + ); } diff --git a/src/components/report-selection/LocalFolderSelector.tsx b/src/components/report-selection/LocalFolderSelector.tsx index 7e4913d9..3b1816fd 100644 --- a/src/components/report-selection/LocalFolderSelector.tsx +++ b/src/components/report-selection/LocalFolderSelector.tsx @@ -132,7 +132,6 @@ const LocalFolderOptions: FC = () => { /> {localUploadLabel} - - // TODO This should live higher in the component stack maybe + {/* TODO This should live higher in the component stack maybe */} - {folderStatus && !isUploading && (
= ({ children }) => { // console.info(`Socket ${eventName}: ${JSON.stringify(data)}`); // }) - return () => { // Cleanup socket listeners on unmount // socket.offAny(); diff --git a/src/model/APIData.ts b/src/model/APIData.ts index 888c7f79..4d87a68d 100644 --- a/src/model/APIData.ts +++ b/src/model/APIData.ts @@ -46,21 +46,21 @@ export interface ActiveReport { } export enum FileStatus { - DOWNLOADING = "DOWNLOADING", - FAILED = "FAILED", - COMPRESSING = "COMPRESSING", - FINISHED = "FINISHED", - STARTED = "STARTED" + DOWNLOADING = 'DOWNLOADING', + FAILED = 'FAILED', + COMPRESSING = 'COMPRESSING', + FINISHED = 'FINISHED', + STARTED = 'STARTED', } // TypeScript Interface with underscored keys to match the backend data export interface FileProgress { current_file_name: string; number_of_files: number; - percent_of_current: number, + percent_of_current: number; finished_files: number; - status: FileStatus; // Use the FileStatus enum - timestamp?: string; // Optional, with default handled elsewhere if necessary + status: FileStatus; // Use the FileStatus enum + timestamp?: string; // Optional, with default handled elsewhere if necessary } // TODO: we may want to revisit the 'default' portion for the variable name diff --git a/src/store/app.ts b/src/store/app.ts index 3d272546..0f443596 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -21,10 +21,9 @@ export const isDramActiveAtom = atom(false); // This atom stores the file transfer progress data in localStorage (or sessionStorage) export const fileTransferProgressAtom = atom({ - currentFileName: '', - numberOfFiles: 0, - percentOfCurrent: 0, - finishedFiles: 0, - status: FileStatus.FINISHED - } -); + currentFileName: '', + numberOfFiles: 0, + percentOfCurrent: 0, + finishedFiles: 0, + status: FileStatus.FINISHED, +}); From a9372c3edb3ade3d9b24e6045f7dcdfdbd8a8b89 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 13:32:32 -0300 Subject: [PATCH 52/67] Adds missing last_emit_time initialization --- backend/ttnn_visualizer/sockets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/ttnn_visualizer/sockets.py b/backend/ttnn_visualizer/sockets.py index 234810c1..459d1e87 100644 --- a/backend/ttnn_visualizer/sockets.py +++ b/backend/ttnn_visualizer/sockets.py @@ -43,6 +43,7 @@ def __post_init__(self): # Global variables for debouncing debounce_timer = None debounce_delay = 0.5 # Delay in seconds (adjust as needed) +last_emit_time = 0 def emit_file_status(progress: FileProgress, tab_id=None): From 8ff43abba3ea1185da4d66ff68e887afa8f82928 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 14:08:52 -0300 Subject: [PATCH 53/67] Adds gevent to requirements.txt --- backend/ttnn_visualizer/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/ttnn_visualizer/requirements.txt b/backend/ttnn_visualizer/requirements.txt index ffef2bae..0a0752cb 100644 --- a/backend/ttnn_visualizer/requirements.txt +++ b/backend/ttnn_visualizer/requirements.txt @@ -10,6 +10,7 @@ setuptools==65.5.0 python-dotenv==1.0.1 flask-sqlalchemy flask-socketio +gevent eventlet flask-session wheel From 9c00f720bc11a121ad97ef88552d632934d44903 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 14:30:46 -0300 Subject: [PATCH 54/67] Replace flask-session with regular SQLAlchemy table --- backend/ttnn_visualizer/app.py | 7 +-- backend/ttnn_visualizer/decorators.py | 2 +- backend/ttnn_visualizer/extensions.py | 5 --- backend/ttnn_visualizer/models.py | 17 ++++++++ backend/ttnn_visualizer/requirements.txt | 1 - backend/ttnn_visualizer/sessions.py | 55 ++++++++++++++++-------- backend/ttnn_visualizer/views.py | 5 ++- 7 files changed, 59 insertions(+), 33 deletions(-) diff --git a/backend/ttnn_visualizer/app.py b/backend/ttnn_visualizer/app.py index 7d31de05..795ef71b 100644 --- a/backend/ttnn_visualizer/app.py +++ b/backend/ttnn_visualizer/app.py @@ -57,7 +57,7 @@ def catch_all(path): def extensions(app: flask.Flask): - from ttnn_visualizer.extensions import flask_static_digest, db, session + from ttnn_visualizer.extensions import flask_static_digest, db """ Register 0 or more extensions (mutates the app passed in). @@ -69,11 +69,6 @@ def extensions(app: flask.Flask): flask_static_digest.init_app(app) db.init_app(app) - app.config["SESSION_TYPE"] = "sqlalchemy" - app.config["SESSION_SQLALCHEMY"] = db - - session.init_app(app) - # Create the tables within the application context with app.app_context(): db.drop_all() diff --git a/backend/ttnn_visualizer/decorators.py b/backend/ttnn_visualizer/decorators.py index 48134f5c..860ba8ab 100644 --- a/backend/ttnn_visualizer/decorators.py +++ b/backend/ttnn_visualizer/decorators.py @@ -18,7 +18,7 @@ def wrapper(*args, **kwargs): abort(404) session = get_or_create_tab_session(tab_id=tab_id) - active_report = session.get("active_report", None) + active_report = session.active_report if not active_report: current_app.logger.error( diff --git a/backend/ttnn_visualizer/extensions.py b/backend/ttnn_visualizer/extensions.py index f7a203bb..43ccdb7e 100644 --- a/backend/ttnn_visualizer/extensions.py +++ b/backend/ttnn_visualizer/extensions.py @@ -1,12 +1,7 @@ from flask_static_digest import FlaskStaticDigest from flask_sqlalchemy import SQLAlchemy -from flask_session import Session flask_static_digest = FlaskStaticDigest() # Initialize Flask SQLAlchemy db = SQLAlchemy() - - -# Initialize Flask-Session -session = Session() diff --git a/backend/ttnn_visualizer/models.py b/backend/ttnn_visualizer/models.py index 2cc7eed6..d31d071a 100644 --- a/backend/ttnn_visualizer/models.py +++ b/backend/ttnn_visualizer/models.py @@ -4,6 +4,9 @@ from json import JSONDecodeError from pydantic import BaseModel, Field +from sqlalchemy import Integer, Column, String, JSON + +from ttnn_visualizer.extensions import db class BufferType(enum.Enum): @@ -165,6 +168,20 @@ class RemoteConnection(BaseModel): path: str +class TabSession(db.Model): + __tablename__ = "tab_sessions" + + id = Column(Integer, primary_key=True) + tab_id = Column(String, unique=True, nullable=False) + active_report = Column(JSON) + remote_connection = Column(JSON, nullable=True) + + def __init__(self, tab_id, active_report, remote_connection=None): + self.tab_id = tab_id + self.active_report = active_report + self.remote_connection = remote_connection + + class StatusMessage(BaseModel): status: int message: str diff --git a/backend/ttnn_visualizer/requirements.txt b/backend/ttnn_visualizer/requirements.txt index 5dffabd4..2cb87a70 100644 --- a/backend/ttnn_visualizer/requirements.txt +++ b/backend/ttnn_visualizer/requirements.txt @@ -9,6 +9,5 @@ flask_static_digest==0.4.1 setuptools==65.5.0 python-dotenv==1.0.1 flask-sqlalchemy -flask-session wheel build diff --git a/backend/ttnn_visualizer/sessions.py b/backend/ttnn_visualizer/sessions.py index e81dd15f..09397b3f 100644 --- a/backend/ttnn_visualizer/sessions.py +++ b/backend/ttnn_visualizer/sessions.py @@ -2,18 +2,21 @@ from flask import request, jsonify, session, current_app +from ttnn_visualizer.models import TabSession +from ttnn_visualizer.extensions import db + logger = getLogger(__name__) def update_tab_session(tab_id, active_report_data, remote_connection_data=None): """ Overwrite the active report for a given tab session or create a new session if one doesn't exist. - Store everything in Flask-Session using tab_id as the key. + Store everything in the database using the TabSession model. """ - # Create or update the active report active_report = {"name": active_report_data.get("name")} # Check if remote connection data is provided and add it to the active report + remote_connection = None if remote_connection_data: remote_connection = { "name": remote_connection_data.get("name"), @@ -22,11 +25,25 @@ def update_tab_session(tab_id, active_report_data, remote_connection_data=None): "port": remote_connection_data.get("port"), "path": remote_connection_data.get("path"), } - active_report["remote_connection"] = remote_connection - # Store the active report in the session using the tab_id as the key - session[tab_id] = {"active_report": active_report} - session.modified = True + # Query the database to find the existing tab session + session_data = TabSession.query.filter_by(tab_id=tab_id).first() + + if session_data: + # Update the session data + session_data.active_report = active_report + session_data.remote_connection = remote_connection + else: + # Create a new session entry + session_data = TabSession( + tab_id=tab_id, + active_report=active_report, + remote_connection=remote_connection, + ) + + db.session.add(session_data) + + db.session.commit() current_app.logger.info(f"Set active report for tab {tab_id} to {active_report}") @@ -38,22 +55,22 @@ def get_or_create_tab_session( ): """ Retrieve an existing tab session or create a new one if it doesn't exist. - Uses Flask-Session for session management with tab_id as the key. - Initializes session data as an empty dictionary if it doesn't exist. + Uses the TabSession model to manage session data. """ - # Check if the session exists for the given tab_id - session_data = session.get(tab_id) + # Query the database for the tab session + session_data = TabSession.query.filter_by(tab_id=tab_id).first() - # If session doesn't exist, initialize it as an empty dictionary + # If session doesn't exist, initialize it if not session_data: - session[tab_id] = {} # Initialize empty session data - session.modified = True - session_data = session.get(tab_id) + session_data = TabSession( + tab_id=tab_id, active_report={}, remote_connection=None + ) + db.session.add(session_data) + db.session.commit() # If active_report_data is provided, update the session with the new report if active_report_data: update_tab_session(tab_id, active_report_data, remote_connection_data) - session.modified = True return session_data @@ -70,14 +87,16 @@ def get_tab_session(): return jsonify({"error": "tabId is required"}), 400 active_report = get_or_create_tab_session(tab_id) - current_app.logger.info(f"get_tab_session: Session retrieved: {active_report}") + current_app.logger.info( + f"get_tab_session: Session retrieved: {active_report.active_report}" + ) + + return jsonify({"active_report": active_report.active_report}), 200 -# Function to initialize the session logic and middleware def init_sessions(app): """ Initializes session middleware and hooks it into Flask. """ app.before_request(get_tab_session) - app.logger.info("Sessions middleware initialized.") diff --git a/backend/ttnn_visualizer/views.py b/backend/ttnn_visualizer/views.py index 8a7590fb..e45cd07a 100644 --- a/backend/ttnn_visualizer/views.py +++ b/backend/ttnn_visualizer/views.py @@ -378,9 +378,10 @@ def get_active_folder(): session = get_or_create_tab_session( tab_id=tab_id ) # Capture both the session and created flag + current_app.logger.info(f"Session: {session}") - if session and session.get("active_report", None): - active_report = session.get("active_report") + if session and session.active_report: + active_report = session.active_report return { "name": active_report.get("name"), "remote_connection": active_report.get("remote_connection", None), From ab662194c87bc1554f41f17bf6de97224f13f06b Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 16:12:23 -0300 Subject: [PATCH 55/67] Fix error after merge (session import) --- backend/ttnn_visualizer/app.py | 5 +---- backend/ttnn_visualizer/sessions.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/ttnn_visualizer/app.py b/backend/ttnn_visualizer/app.py index 14ef6b27..971ce3ff 100644 --- a/backend/ttnn_visualizer/app.py +++ b/backend/ttnn_visualizer/app.py @@ -59,7 +59,7 @@ def catch_all(path): def extensions(app: flask.Flask): - from ttnn_visualizer.extensions import flask_static_digest, db, session, socketio + from ttnn_visualizer.extensions import flask_static_digest, db, socketio from ttnn_visualizer.sockets import register_handlers """ @@ -79,9 +79,6 @@ def extensions(app: flask.Flask): with app.app_context(): db.drop_all() - session.init_app(app) - # Register the handlers by passing the socketio instance - register_handlers(socketio) # Create the tables within the application context diff --git a/backend/ttnn_visualizer/sessions.py b/backend/ttnn_visualizer/sessions.py index 09397b3f..37cfdf5b 100644 --- a/backend/ttnn_visualizer/sessions.py +++ b/backend/ttnn_visualizer/sessions.py @@ -1,6 +1,6 @@ from logging import getLogger -from flask import request, jsonify, session, current_app +from flask import request, jsonify, current_app from ttnn_visualizer.models import TabSession from ttnn_visualizer.extensions import db From 90b3c653ce3ced76239e0e5a87803de56180d8b3 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 16:26:21 -0300 Subject: [PATCH 56/67] Adds wrapper component for socket connection --- src/components/FileStatusOverlay.tsx | 69 ++++--------------- src/components/FileStatusOverlayWrapper.tsx | 66 ++++++++++++++++++ .../report-selection/LocalFolderSelector.tsx | 6 +- src/model/APIData.ts | 9 +-- 4 files changed, 88 insertions(+), 62 deletions(-) create mode 100644 src/components/FileStatusOverlayWrapper.tsx diff --git a/src/components/FileStatusOverlay.tsx b/src/components/FileStatusOverlay.tsx index 5d5995a0..2e9aecdf 100644 --- a/src/components/FileStatusOverlay.tsx +++ b/src/components/FileStatusOverlay.tsx @@ -2,84 +2,41 @@ // // SPDX-FileCopyrightText: © 2024 Tenstorrent Inc. -import { useAtom } from 'jotai'; -import { useContext, useEffect, useState } from 'react'; +import React from 'react'; import Overlay from './Overlay'; import ProgressBar from './ProgressBar'; import 'styles/components/FileStatusOverlay.scss'; -import { SocketContext, SocketContextType } from '../libs/SocketProvider'; -import { getOrCreateTabId } from '../libs/axiosInstance'; import { FileProgress, FileStatus } from '../model/APIData'; -import { fileTransferProgressAtom } from '../store/app'; -interface FileStatusOverlayProps { - canEscapeKeyClose?: boolean; +interface FileTransferOverlayProps { + progress: FileProgress; } -function FileStatusOverlay({ canEscapeKeyClose = false }: FileStatusOverlayProps) { - const [fileTransferProgress, setFileTransferProgress] = useAtom(fileTransferProgressAtom); - const [tabId] = useState(getOrCreateTabId()); - const socket = useContext(SocketContext); - +const FileStatusOverlay: React.FC = ({ progress }) => { const formatPercentage = (percentage: number) => percentage.toFixed(2).padStart(5, '0'); - useEffect(() => { - if (!socket) { - return; - } - - socket.on('connect', () => { - // Reset the progress when the socket reconnects (possible HMR case) - setFileTransferProgress({ - currentFileName: '', - numberOfFiles: 0, - percentOfCurrent: 0, - finishedFiles: 0, - status: FileStatus.FINISHED, - }); - }); + const { status, currentFileName, finishedFiles, numberOfFiles, percentOfCurrent } = progress; - // Listen for file transfer progress messages - socket.on('fileTransferProgress', (data: FileProgress & { tab_id: string }) => { - if (data.tab_id === tabId) { - setFileTransferProgress({ - currentFileName: data.current_file_name, - numberOfFiles: data.number_of_files, - percentOfCurrent: data.percent_of_current, - finishedFiles: data.finished_files, - status: data.status, - }); - } - }); - - // Clean up the WebSocket connection on component unmount - // eslint-disable-next-line consistent-return - return () => { - socket.off('fileTransferProgress'); - }; - }, [socket, setFileTransferProgress, tabId]); return (

File Transfer Progress

-

Current File: {fileTransferProgress.currentFileName}

+

Current File: {currentFileName}

- Files Transferred: {fileTransferProgress.finishedFiles}/{fileTransferProgress.numberOfFiles} + Files Transferred: {finishedFiles}/{numberOfFiles}

-

Current File Progress: {formatPercentage(fileTransferProgress.percentOfCurrent)}%

-

Status: {fileTransferProgress.status}

+

Current File Progress: {formatPercentage(percentOfCurrent)}%

+

Status: {status}

- +
); -} +}; export default FileStatusOverlay; diff --git a/src/components/FileStatusOverlayWrapper.tsx b/src/components/FileStatusOverlayWrapper.tsx new file mode 100644 index 00000000..54843a21 --- /dev/null +++ b/src/components/FileStatusOverlayWrapper.tsx @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: © 2024 Tenstorrent Inc. + +import React, { useContext, useEffect, useState } from 'react'; +import { SocketContext, SocketContextType } from '../libs/SocketProvider'; +import { FileProgress, FileStatus } from '../model/APIData'; +import { getOrCreateTabId } from '../libs/axiosInstance'; + +interface FileTransferStatusWrapperProps { + children: (fileProgress: FileProgress) => React.ReactNode; +} + +const FileStatusWrapper: React.FC = ({ children }) => { + const [fileTransferProgress, setFileTransferProgress] = useState({ + currentFileName: '', + numberOfFiles: 0, + percentOfCurrent: 0, + finishedFiles: 0, + status: FileStatus.INACTIVE, + }); + + const [tabId] = useState(getOrCreateTabId()); + const socket = useContext(SocketContext); + + useEffect(() => { + if (!socket) { + return; + } + + // Handle socket connection event + socket.on('connect', () => { + setFileTransferProgress({ + currentFileName: '', + numberOfFiles: 0, + percentOfCurrent: 0, + finishedFiles: 0, + status: FileStatus.INACTIVE, + }); + }); + + // Handle file transfer progress from the socket + socket.on('fileTransferProgress', (data) => { + if (data.tab_id === tabId) { + setFileTransferProgress({ + currentFileName: data.current_file_name, + numberOfFiles: data.number_of_files, + percentOfCurrent: data.percent_of_current, + finishedFiles: data.finished_files, + status: data.status, + }); + } + }); + + // Cleanup socket listeners on unmount + // eslint-disable-next-line consistent-return + return () => { + socket.off('fileTransferProgress'); + }; + }, [socket, tabId]); + + // Call the children render prop with the file transfer progress + return <>{children(fileTransferProgress)}; +}; + +export default FileStatusWrapper; diff --git a/src/components/report-selection/LocalFolderSelector.tsx b/src/components/report-selection/LocalFolderSelector.tsx index 3b1816fd..332e4113 100644 --- a/src/components/report-selection/LocalFolderSelector.tsx +++ b/src/components/report-selection/LocalFolderSelector.tsx @@ -14,6 +14,7 @@ import ROUTES from '../../definitions/routes'; import useLocalConnection from '../../hooks/useLocal'; import { reportLocationAtom } from '../../store/app'; import { ConnectionStatus, ConnectionTestStates } from '../../definitions/ConnectionStatus'; +import FileStatusWrapper from '../FileStatusOverlayWrapper'; import FileStatusOverlay from '../FileStatusOverlay'; const ICON_MAP: Record = { @@ -139,9 +140,10 @@ const LocalFolderOptions: FC = () => { > View report + + {(fileProgress) => } + - {/* TODO This should live higher in the component stack maybe */} - {folderStatus && !isUploading && (
Date: Tue, 15 Oct 2024 16:28:18 -0300 Subject: [PATCH 57/67] Adds an open property to avoid delay in showing progress --- src/components/FileStatusOverlay.tsx | 5 +++-- src/components/report-selection/LocalFolderSelector.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/FileStatusOverlay.tsx b/src/components/FileStatusOverlay.tsx index 2e9aecdf..3726e269 100644 --- a/src/components/FileStatusOverlay.tsx +++ b/src/components/FileStatusOverlay.tsx @@ -10,16 +10,17 @@ import { FileProgress, FileStatus } from '../model/APIData'; interface FileTransferOverlayProps { progress: FileProgress; + open?: boolean; } -const FileStatusOverlay: React.FC = ({ progress }) => { +const FileStatusOverlay: React.FC = ({ progress, open }) => { const formatPercentage = (percentage: number) => percentage.toFixed(2).padStart(5, '0'); const { status, currentFileName, finishedFiles, numberOfFiles, percentOfCurrent } = progress; return ( { View report - {(fileProgress) => } + {(fileProgress) => ( + + )} {folderStatus && !isUploading && ( From 298f3e7ff6e09dd9f4ef8254c34a1d08791e4c63 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Tue, 15 Oct 2024 16:31:37 -0300 Subject: [PATCH 58/67] Move open prop outside of overlay component --- src/components/FileStatusOverlay.tsx | 6 +++--- src/components/report-selection/LocalFolderSelector.tsx | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/FileStatusOverlay.tsx b/src/components/FileStatusOverlay.tsx index 3726e269..394d6807 100644 --- a/src/components/FileStatusOverlay.tsx +++ b/src/components/FileStatusOverlay.tsx @@ -6,11 +6,11 @@ import React from 'react'; import Overlay from './Overlay'; import ProgressBar from './ProgressBar'; import 'styles/components/FileStatusOverlay.scss'; -import { FileProgress, FileStatus } from '../model/APIData'; +import { FileProgress } from '../model/APIData'; interface FileTransferOverlayProps { progress: FileProgress; - open?: boolean; + open: boolean; } const FileStatusOverlay: React.FC = ({ progress, open }) => { @@ -20,7 +20,7 @@ const FileStatusOverlay: React.FC = ({ progress, open return ( = { [ConnectionTestStates.IDLE]: IconNames.DOT, @@ -143,7 +144,12 @@ const LocalFolderOptions: FC = () => { {(fileProgress) => ( )} From 57ea9ce91e82ea67343ad7c9578e082d1dc64e49 Mon Sep 17 00:00:00 2001 From: David Blundell Date: Tue, 15 Oct 2024 16:03:12 -0400 Subject: [PATCH 59/67] Added headings and a scroll to nav --- .../BufferSummaryPlotRenderer.tsx | 1 + src/routes/BufferSummary.tsx | 80 +++++++++++++++++-- src/scss/components/BufferSummary.scss | 24 ++++++ 3 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 src/scss/components/BufferSummary.scss diff --git a/src/components/buffer-summary/BufferSummaryPlotRenderer.tsx b/src/components/buffer-summary/BufferSummaryPlotRenderer.tsx index dce7bd0b..5aa64e32 100644 --- a/src/components/buffer-summary/BufferSummaryPlotRenderer.tsx +++ b/src/components/buffer-summary/BufferSummaryPlotRenderer.tsx @@ -13,6 +13,7 @@ import MemoryPlotRenderer from '../operation-details/MemoryPlotRenderer'; import LoadingSpinner from '../LoadingSpinner'; import BufferSummaryRow from './BufferSummaryRow'; import { HistoricalTensor, Operation, Tensor } from '../../model/Graph'; +import 'styles/components/BufferSummaryPlot.scss'; const PLACEHOLDER_ARRAY_SIZE = 30; const OPERATION_EL_HEIGHT = 20; // Height in px of each list item diff --git a/src/routes/BufferSummary.tsx b/src/routes/BufferSummary.tsx index 43b8ac91..0072d60e 100644 --- a/src/routes/BufferSummary.tsx +++ b/src/routes/BufferSummary.tsx @@ -3,17 +3,28 @@ // SPDX-FileCopyrightText: © 2024 Tenstorrent AI ULC import { Helmet } from 'react-helmet-async'; +import { AnchorButton, ButtonGroup, Intent } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; import { useSetAtom } from 'jotai'; -import { useEffect } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useReportMeta } from '../hooks/useAPI'; import { reportMetaAtom } from '../store/app'; -import 'styles/components/BufferSummaryPlot.scss'; +import 'styles/components/BufferSummary.scss'; import BufferSummaryPlotRenderer from '../components/buffer-summary/BufferSummaryPlotRenderer'; import BufferSummaryTable from '../components/buffer-summary/BufferSummaryTable'; +import ROUTES from '../definitions/routes'; + +const SECTION_IDS = { + PLOT: 'plot', + TABLE: 'table', +}; export default function BufferSummary() { const report = useReportMeta(); const setMeta = useSetAtom(reportMetaAtom); + const plotRef = useRef(null); + const tableRef = useRef(null); + const [activeSection, setActiveSection] = useState(SECTION_IDS.PLOT); // Needs to be in a useEffect to avoid a bad setState call useEffect(() => { @@ -22,13 +33,70 @@ export default function BufferSummary() { } }, [report, setMeta]); + useEffect(() => { + const scrollRefs = [plotRef, tableRef]; + + function navHighlighter() { + const { scrollY } = window; + + scrollRefs.forEach((ref) => { + if (ref?.current?.offsetHeight && ref?.current?.offsetTop) { + const sectionHeight = ref.current.offsetHeight; + const sectionTop = ref.current.offsetTop - 250; + const sectionId = ref.current.getAttribute('id'); + + if (sectionId && scrollY > sectionTop && scrollY <= sectionTop + sectionHeight) { + setActiveSection(sectionId); + } + } + }); + } + + window.addEventListener('scroll', navHighlighter); + return () => window.removeEventListener('scroll', navHighlighter); + }, []); + return ( - <> +
- +

All buffers by operation

+ + + + Plot view + + + + Table view + + + +

Plot view

+
+ +
- - +

Table view

+
+ +
+
); } diff --git a/src/scss/components/BufferSummary.scss b/src/scss/components/BufferSummary.scss new file mode 100644 index 00000000..b09196cd --- /dev/null +++ b/src/scss/components/BufferSummary.scss @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// SPDX-FileCopyrightText: © 2024 Tenstorrent Inc. +@use '../definitions/colours' as *; + +.buffer-summary { + position: relative; + + .page-title { + font-size: 24px; + margin-top: 0; + } + + .sticky-nav { + display: block; + position: sticky; + top: 0; + left: 0; + z-index: 1; + background-color: $tt-background; + padding: 5px 0; + } + +} From c3b8fbc13ae7b082476d256ed08931fb8ec1380e Mon Sep 17 00:00:00 2001 From: Denis Kartashevskiy Date: Wed, 16 Oct 2024 09:28:00 -0400 Subject: [PATCH 60/67] Buffer view interactivity - linkable operation ids --- .../buffer-summary/BufferSummaryPlotRenderer.tsx | 10 ++++++++-- src/scss/components/BufferSummaryPlot.scss | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/buffer-summary/BufferSummaryPlotRenderer.tsx b/src/components/buffer-summary/BufferSummaryPlotRenderer.tsx index 5aa64e32..be3a5bf9 100644 --- a/src/components/buffer-summary/BufferSummaryPlotRenderer.tsx +++ b/src/components/buffer-summary/BufferSummaryPlotRenderer.tsx @@ -6,6 +6,7 @@ import { UIEvent, useMemo, useRef, useState } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import classNames from 'classnames'; import { Switch } from '@blueprintjs/core'; +import { Link } from 'react-router-dom'; import { BufferSummaryAxisConfiguration } from '../../definitions/PlotConfigurations'; import { BuffersByOperationData, useBuffers, useDevices, useOperationsList } from '../../hooks/useAPI'; import { BufferType } from '../../model/BufferType'; @@ -14,6 +15,7 @@ import LoadingSpinner from '../LoadingSpinner'; import BufferSummaryRow from './BufferSummaryRow'; import { HistoricalTensor, Operation, Tensor } from '../../model/Graph'; import 'styles/components/BufferSummaryPlot.scss'; +import ROUTES from '../../definitions/routes'; const PLACEHOLDER_ARRAY_SIZE = 30; const OPERATION_EL_HEIGHT = 20; // Height in px of each list item @@ -153,8 +155,12 @@ function BufferSummaryPlotRenderer() { memoryEnd={isZoomedIn ? zoomedMemorySizeEnd : memorySize} memoryPadding={memoryPadding} /> - -

{operation.id}

+ + {operation.id} +
); })} diff --git a/src/scss/components/BufferSummaryPlot.scss b/src/scss/components/BufferSummaryPlot.scss index eb487105..44ef8f2f 100644 --- a/src/scss/components/BufferSummaryPlot.scss +++ b/src/scss/components/BufferSummaryPlot.scss @@ -44,6 +44,12 @@ $height-offset: 15px; margin-bottom: 0; text-align: right; flex-shrink: 0; + color: $tt-white; + text-decoration: none; + + &:hover { + text-decoration: underline; + } } } From 7979f4271589101cffc392d641776c2bc534016b Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 16 Oct 2024 10:44:49 -0300 Subject: [PATCH 61/67] Update requirements.txt/pyproject.toml dependencies --- backend/ttnn_visualizer/requirements.txt | 3 +-- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/ttnn_visualizer/requirements.txt b/backend/ttnn_visualizer/requirements.txt index 0a0752cb..d6e9652d 100644 --- a/backend/ttnn_visualizer/requirements.txt +++ b/backend/ttnn_visualizer/requirements.txt @@ -10,8 +10,7 @@ setuptools==65.5.0 python-dotenv==1.0.1 flask-sqlalchemy flask-socketio -gevent -eventlet +gevent==24.10.2 flask-session wheel build diff --git a/pyproject.toml b/pyproject.toml index 006db3c8..58dddb6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,16 +8,16 @@ requires-python = ">=3.12" dependencies = [ "flask", "gunicorn~=22.0.0", - "uvicorn==0.30.1", "paramiko~=3.4.0", - "marshmallow==3.21.3", - "flask_marshmallow==1.2.1", "flask_cors==4.0.1", "pydantic==2.7.3", "pydantic_core==2.18.4", "flask_static_digest==0.4.1", "setuptools==65.5.0", + "gevent===24.10.1", "python-dotenv==1.0.1", + "sqlalchemy==2.0.34", + "flask-socketio==5.4.1" ] classifiers = [ From c2924ff1e8e974ab5702d173de2ad16f1b025334 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 16 Oct 2024 10:45:17 -0300 Subject: [PATCH 62/67] Update GUNICORN bind to use settings module --- backend/ttnn_visualizer/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/ttnn_visualizer/settings.py b/backend/ttnn_visualizer/settings.py index 4af1f810..cda07cfc 100644 --- a/backend/ttnn_visualizer/settings.py +++ b/backend/ttnn_visualizer/settings.py @@ -33,7 +33,7 @@ class DefaultConfig(object): # Gunicorn settings GUNICORN_WORKER_CLASS = os.getenv("GUNICORN_WORKER_CLASS", "gevent") GUNICORN_WORKERS = os.getenv("GUNICORN_WORKERS", "1") - GUNICORN_BIND = os.getenv("GUNICORN_BIND", "localhost:8000") + GUNICORN_BIND = f"0.0.0.0:{os.getenv('PORT', '8000')}" GUNICORN_APP_MODULE = os.getenv( "GUNICORN_APP_MODULE", "ttnn_visualizer.app:create_app()" ) From 82356313518269890ba5801850f492938bab827d Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 16 Oct 2024 10:46:36 -0300 Subject: [PATCH 63/67] Add name to operation buffers API endpoint --- backend/ttnn_visualizer/serializers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/ttnn_visualizer/serializers.py b/backend/ttnn_visualizer/serializers.py index 061d602e..d383d29d 100644 --- a/backend/ttnn_visualizer/serializers.py +++ b/backend/ttnn_visualizer/serializers.py @@ -2,7 +2,7 @@ from collections import defaultdict from email.policy import default -from ttnn_visualizer.models import BufferType +from ttnn_visualizer.models import BufferType, Operation def serialize_operations( @@ -152,12 +152,16 @@ def serialize_operation( } -def serialize_operation_buffers(operation, operation_buffers): +def serialize_operation_buffers(operation: Operation, operation_buffers): buffer_data = [dataclasses.asdict(b) for b in operation_buffers] for b in buffer_data: b.pop("operation_id") b.update({"size": b.pop("max_size_per_bank")}) - return {"id": operation.operation_id, "buffers": list(buffer_data)} + return { + "id": operation.operation_id, + "name": operation.name, + "buffers": list(buffer_data), + } def serialize_devices(devices): From 483659dfc736c21c584830a62893577511e9d366 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 16 Oct 2024 10:50:18 -0300 Subject: [PATCH 64/67] Update failing test --- backend/ttnn_visualizer/tests/test_queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/ttnn_visualizer/tests/test_queries.py b/backend/ttnn_visualizer/tests/test_queries.py index 7b923859..80902a60 100644 --- a/backend/ttnn_visualizer/tests/test_queries.py +++ b/backend/ttnn_visualizer/tests/test_queries.py @@ -178,7 +178,7 @@ def test_query_buffer_pages(self): def test_query_buffers(self): self.connection.execute("INSERT INTO buffers VALUES (1, 1, 0, 1024, 0)") - buffers = list(self.db_queries.query_buffers(1)) + buffers = list(self.db_queries.query_buffers()) self.assertEqual(len(buffers), 1) buffer = buffers[0] self.assertIsInstance(buffer, Buffer) From dd77390a3ad187d0579e6bbd805562fff3c36806 Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 16 Oct 2024 10:53:38 -0300 Subject: [PATCH 65/67] Add last missing dependency to pyproject.toml --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 58dddb6e..0d31a209 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,8 @@ dependencies = [ "gevent===24.10.1", "python-dotenv==1.0.1", "sqlalchemy==2.0.34", - "flask-socketio==5.4.1" + "flask-socketio==5.4.1", + "flask-sqlalchemy==3.1.1" ] classifiers = [ From 357bfc1fd38e5cd3633752007b277ce80a0ea27f Mon Sep 17 00:00:00 2001 From: Greg Hatt Date: Wed, 16 Oct 2024 11:20:38 -0300 Subject: [PATCH 66/67] Update gunicorn config with worker configuration --- backend/ttnn_visualizer/config/gunicorn.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/backend/ttnn_visualizer/config/gunicorn.py b/backend/ttnn_visualizer/config/gunicorn.py index cbff5a4f..83b53474 100644 --- a/backend/ttnn_visualizer/config/gunicorn.py +++ b/backend/ttnn_visualizer/config/gunicorn.py @@ -16,11 +16,8 @@ accesslog = "-" access_log_format = "%(h)s %(l)s %(u)s %(t)s '%(r)s' %(s)s %(b)s '%(f)s' '%(a)s' in %(D)sµs" # noqa: E501 -workers = 1 -threads = 1 +reload = bool(str_to_bool(os.getenv("WEB_RELOAD", "false"))) -# Currently no need for multithreading/workers -# workers = int(os.getenv("WEB_CONCURRENCY", multiprocessing.cpu_count() * 2)) -# threads = int(os.getenv("PYTHON_MAX_THREADS", 1)) +worker_class = "gevent" -reload = bool(str_to_bool(os.getenv("WEB_RELOAD", "false"))) +workers = 1 From 032c899a1c4b87593a7251f636d80d9b68d641ac Mon Sep 17 00:00:00 2001 From: Denis Kartashevskiy Date: Wed, 16 Oct 2024 10:51:44 -0400 Subject: [PATCH 67/67] v0.6.0 --- package-lock.json | 85 +++++++++++++++++++++++++++++++++++++++++++++-- package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index da9d1de2..83229327 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ttnn-visualzer", - "version": "0.5.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ttnn-visualzer", - "version": "0.5.0", + "version": "0.6.0", "dependencies": { "@blueprintjs/core": "^5.10.3", "@blueprintjs/select": "^5.1.5", @@ -25,6 +25,7 @@ "react-router": "^6.23.1", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", + "socket.io-client": "^4.8.0", "tinycolor2": "^1.6.0" }, "devDependencies": { @@ -3109,6 +3110,11 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@tanstack/react-virtual": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.5.1.tgz", @@ -5489,7 +5495,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5853,6 +5858,26 @@ "once": "^1.4.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", @@ -14586,6 +14611,32 @@ "tslib": "^2.0.3" } }, + "node_modules/socket.io-client": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -16455,6 +16506,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xmlbuilder": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", @@ -16464,6 +16535,14 @@ "node": ">=6.0" } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 3dce46f0..d35e1bbe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ttnn-visualzer", "private": true, - "version": "0.5.0", + "version": "0.6.0", "type": "module", "scripts": { "dev": "vite", diff --git a/pyproject.toml b/pyproject.toml index 0d31a209..5c1a03c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ttnn_visualizer" authors = [] -version = "0.5.0" +version = "0.6.0" description = "TT Visualizer" readme = "README.md" requires-python = ">=3.12"