Working on improving the perf

This commit is contained in:
2025-08-10 17:42:54 +02:00
parent 5820efb7f1
commit 67abb45804
12 changed files with 142 additions and 69 deletions

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyInitNewSignatureInspection" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/MyManagingTools.iml" filepath="$PROJECT_DIR$/.idea/MyManagingTools.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,6 +1,8 @@
import asyncio
import json import json
import logging import logging
from fasthtml.components import Div, sse_message
from fasthtml.core import EventStream from fasthtml.core import EventStream
from fasthtml.fastapp import fast_app from fasthtml.fastapp import fast_app
from starlette.datastructures import UploadFile from starlette.datastructures import UploadFile
@@ -138,9 +140,21 @@ def post(session, _id: str, state: str, args: str = None):
instance = InstanceManager.get(session, _id) instance = InstanceManager.get(session, _id)
return instance.manage_state_changed(state, args) return instance.manage_state_changed(state, args)
@rt(Routes.YieldRow) @rt(Routes.YieldRow)
async def get(session, _id: str): async def get(session, _id: str):
logger.debug(f"Entering {Routes.YieldRow} with args {_id=}") logger.debug(f"Entering {Routes.YieldRow} with args {_id=}")
instance = InstanceManager.get(session, _id) instance = InstanceManager.get(session, _id)
return EventStream(instance.mk_async_lasy_body_content()) return EventStream(instance.mk_lazy_body_content())
async def number_generator2():
for i in range(20):
yield sse_message(Div(i * 5 + 1))
yield sse_message(Div(i * 5 + 2))
yield sse_message(Div(i * 5 + 3))
yield sse_message(Div(i * 5 + 4))
yield sse_message(Div(i * 5 + 5))
await asyncio.sleep(0.1)
yield f"event: close\ndata: \n\n"

View File

@@ -1,6 +1,10 @@
function bindDatagrid(datagridId, allowColumnsReordering) { function bindDatagrid(datagridId, allowColumnsReordering) {
bindScrollbars(datagridId); bindScrollbars(datagridId);
makeResizable(datagridId) makeResizable(datagridId)
document.body.addEventListener('htmx:sseBeforeMessage', function (e) {
console.log("htmx:sseBeforeMessage", e)
})
} }
function bindScrollbars(datagridId) { function bindScrollbars(datagridId) {
@@ -494,4 +498,5 @@ function onAfterSettle(datagridId, event) {
if (response.includes("hx-on::before-settle")) { if (response.includes("hx-on::before-settle")) {
bindDatagrid(datagridId) bindDatagrid(datagridId)
} }
} }

View File

@@ -1,3 +1,4 @@
import asyncio
import copy import copy
import logging import logging
from io import BytesIO from io import BytesIO
@@ -23,7 +24,7 @@ from components_helpers import mk_icon, mk_ellipsis
from core.fasthtml_helper import MyDiv, mk_my_ellipsis, MySpan, mk_my_icon from core.fasthtml_helper import MyDiv, mk_my_ellipsis, MySpan, mk_my_icon
from core.instance_manager import InstanceManager from core.instance_manager import InstanceManager
from core.settings_management import SettingsManager from core.settings_management import SettingsManager
from core.utils import get_unique_id, make_safe_id, timed, profile_function from core.utils import get_unique_id, make_safe_id, timed
logger = logging.getLogger("DataGrid") logger = logging.getLogger("DataGrid")
@@ -60,6 +61,7 @@ class DataGrid(BaseComponent):
self._state: DataGridState = self._db.load_state() self._state: DataGridState = self._db.load_state()
self._settings: DataGridSettings = grid_settings or self._db.load_settings() self._settings: DataGridSettings = grid_settings or self._db.load_settings()
self._df: DataFrame | None = self._db.load_dataframe() self._df: DataFrame | None = self._db.load_dataframe()
self._fast_access = self._init_fast_access(self._df)
# update boundaries if possible # update boundaries if possible
self.set_boundaries(boundaries) self.set_boundaries(boundaries)
@@ -127,6 +129,7 @@ class DataGrid(BaseComponent):
col_id, col_id,
_get_column_type(self._df[make_safe_id(col_id)].dtype)) _get_column_type(self._df[make_safe_id(col_id)].dtype))
for col_index, col_id in enumerate(df.columns)] for col_index, col_id in enumerate(df.columns)]
self._fast_access = self._init_fast_access(self._df)
if save_state: if save_state:
self._db.save_all(None, self._state, self._df) self._db.save_all(None, self._state, self._df)
@@ -387,7 +390,7 @@ class DataGrid(BaseComponent):
id=f"scb_{self._id}", id=f"scb_{self._id}",
) )
@profile_function @timed
def mk_table(self, oob=False): def mk_table(self, oob=False):
htmx_extra_params = { htmx_extra_params = {
"hx-on::before-settle": f"onAfterSettle('{self._id}', event);", "hx-on::before-settle": f"onAfterSettle('{self._id}', event);",
@@ -441,8 +444,8 @@ class DataGrid(BaseComponent):
_mk_keyboard_management(), _mk_keyboard_management(),
Div( Div(
self.mk_table_header(), self.mk_table_header(),
# self.mk_table_body(), #self.mk_table_body(),
self.mk_table_body_lasy(), self.mk_table_body_lazy(),
self.mk_table_footer(), self.mk_table_footer(),
cls="dt2-inner-table"), cls="dt2-inner-table"),
cls="dt2-table", cls="dt2-table",
@@ -477,25 +480,25 @@ class DataGrid(BaseComponent):
header_class = "dt2-row dt2-header" + "" if self._settings.header_visible else " hidden" header_class = "dt2-row dt2-header" + "" if self._settings.header_visible else " hidden"
return Div( return Div(
Div(sse_swap="message"),
*[_mk_header(col_def) for col_def in self._state.columns], *[_mk_header(col_def) for col_def in self._state.columns],
cls=header_class, cls=header_class,
id=f"th_{self._id}" id=f"th_{self._id}"
) )
def mk_table_body_lasy(self): def mk_table_body_lazy(self):
df = self._get_filtered_df()
max_height = self._compute_body_max_height() max_height = self._compute_body_max_height()
return Div( return Div(
# *self.mk_lasy_body_content(df),
hx_ext="sse", hx_ext="sse",
sse_connect=f"{ROUTE_ROOT}{Routes.YieldRow}", sse_connect=f"{ROUTE_ROOT}{Routes.YieldRow}?_id={self._id}",
hx_swap="beforeend show:bottom", sse_close='close',
sse_swap="message", sse_swap="message",
hx_swap="beforeend",
cls="dt2-body", cls="dt2-body",
style=f"max-height:{max_height}px;", style=f"max-height:{max_height}px;",
id=f"tb_{self._id}", id=f"tb_{self._id}",
hx_vals=f'{{"_id": "{self._id}"}}',
) )
def mk_table_body(self): def mk_table_body(self):
@@ -526,22 +529,21 @@ class DataGrid(BaseComponent):
id=f"tf_{self._id}" id=f"tf_{self._id}"
) )
async def mk_async_lasy_body_content(self, df): async def mk_lazy_body_content(self):
for row_index in df.index: df = self._get_filtered_df()
for i, row_index in enumerate(df.index):
yield sse_message(Div( yield sse_message(Div(
*[self.mk_body_cell(col_pos, row_index, col_def) for col_pos, col_def in enumerate(self._state.columns)], *[self.mk_body_cell(col_pos, row_index, col_def) for col_pos, col_def in enumerate(self._state.columns)],
cls="dt2-row", cls="dt2-row",
data_row=f"{row_index}", data_row=f"{row_index}",
id=f"tr_{self._id}-{row_index}", id=f"tr_{self._id}-{row_index}",
)) ))
if i % 50 == 0:
def mk_lasy_body_content(self, df): await asyncio.sleep(0.01)
return [Div( logger.debug(f"yielding row {i}")
*[self.mk_body_cell(col_pos, row_index, col_def) for col_pos, col_def in enumerate(self._state.columns)],
cls="dt2-row", logger.debug(f"yielding close event")
data_row=f"{row_index}", yield f"event: close\ndata: \n\n"
id=f"tr_{self._id}-{row_index}",
) for row_index in df.index]
def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState): def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState):
if not col_def.usable: if not col_def.usable:
@@ -557,21 +559,20 @@ class DataGrid(BaseComponent):
style=f"width:{col_def.width}px;", style=f"width:{col_def.width}px;",
cls="dt2-cell") cls="dt2-cell")
@profile_function
def mk_body_cell_content(self, col_pos, row_index, col_def: DataGridColumnState): def mk_body_cell_content(self, col_pos, row_index, col_def: DataGridColumnState):
def mk_bool(value): def mk_bool(_value):
return MyDiv(mk_my_icon(icon_checked if value else icon_unchecked, can_select=False), return MyDiv(mk_my_icon(icon_checked if _value else icon_unchecked, can_select=False),
cls="dt2-cell-content-checkbox") cls="dt2-cell-content-checkbox")
def mk_text(value): def mk_text(_value):
return mk_my_ellipsis(value, cls="dt2-cell-content-text") return mk_my_ellipsis(_value, cls="dt2-cell-content-text")
def mk_number(value): def mk_number(_value):
return mk_my_ellipsis(value, cls="dt2-cell-content-number") return mk_my_ellipsis(_value, cls="dt2-cell-content-number")
def process_cell_content(value): def process_cell_content(_value):
value_str = str(value) value_str = str(_value)
if FILTER_INPUT_CID not in self._state.filtered or ( if FILTER_INPUT_CID not in self._state.filtered or (
keyword := self._state.filtered[FILTER_INPUT_CID]) is None: keyword := self._state.filtered[FILTER_INPUT_CID]) is None:
@@ -588,15 +589,17 @@ class DataGrid(BaseComponent):
return tuple(res) return tuple(res)
column_type = col_def.type column_type = col_def.type
# value = self._df.iloc[row_index, col_def.col_index]
value = self._fast_access[col_def.col_id][row_index]
if column_type == ColumnType.Bool: if column_type == ColumnType.Bool:
content = mk_bool(self._df.iloc[row_index, col_def.col_index]) content = mk_bool(value)
elif column_type == ColumnType.Number: elif column_type == ColumnType.Number:
content = mk_number(process_cell_content(self._df.iloc[row_index, col_def.col_index])) content = mk_number(process_cell_content(value))
elif column_type == ColumnType.RowIndex: elif column_type == ColumnType.RowIndex:
content = mk_number(row_index) content = mk_number(row_index)
else: else:
content = mk_text(process_cell_content(self._df.iloc[row_index, col_def.col_index])) content = mk_text(process_cell_content(value))
return content return content
@@ -859,6 +862,25 @@ class DataGrid(BaseComponent):
return True return True
@staticmethod
def _init_fast_access(df):
"""
Generates a fast-access dictionary for a DataFrame.
This method converts the columns of the provided DataFrame into NumPy arrays
and stores them as values in a dictionary, using the column names as keys.
This allows for efficient access to the data stored in the DataFrame.
Args:
df (DataFrame): The input pandas DataFrame whose columns are to be converted
into a dictionary of NumPy arrays.
Returns:
dict: A dictionary where the keys are the column names of the input DataFrame
and the values are the corresponding column values as NumPy arrays.
"""
return {col: df[col].to_numpy() for col in df.columns}
@timed @timed
def __ft__(self): def __ft__(self):
return Div( return Div(

View File

@@ -20,7 +20,7 @@ def get(session):
@rt(Routes.AddRepository) @rt(Routes.AddRepository)
def post(session, _id: str, tab_id: str, form_id: str, repository: str, table: str, tab_boundaries:str): def post(session, _id: str, tab_id: str, form_id: str, repository: str, table: str, tab_boundaries: str):
logger.debug( logger.debug(
f"Entering {Routes.AddRepository} with args {debug_session(session)}, {_id=}, {tab_id=}, {form_id=}, {repository=}, {table=}, {tab_boundaries=}") f"Entering {Routes.AddRepository} with args {debug_session(session)}, {_id=}, {tab_id=}, {form_id=}, {repository=}, {table=}, {tab_boundaries=}")
instance = InstanceManager.get(session, _id) # Repository instance = InstanceManager.get(session, _id) # Repository
@@ -34,8 +34,9 @@ def get(session, _id: str, repository_name: str):
@rt(Routes.AddTable) @rt(Routes.AddTable)
def post(session, _id: str, tab_id: str, form_id: str, repository_name: str, table_name: str, tab_boundaries:str): def post(session, _id: str, tab_id: str, form_id: str, repository_name: str, table_name: str, tab_boundaries: str):
logger.debug(f"Entering {Routes.AddTable} with args {debug_session(session)}, {_id=}, {tab_id=}, {form_id=}, {repository_name=}, {table_name=}, {tab_boundaries=}") logger.debug(
f"Entering {Routes.AddTable} with args {debug_session(session)}, {_id=}, {tab_id=}, {form_id=}, {repository_name=}, {table_name=}, {tab_boundaries=}")
instance = InstanceManager.get(session, _id) instance = InstanceManager.get(session, _id)
return instance.add_new_table(tab_id, form_id, repository_name, table_name, json.loads(tab_boundaries)) return instance.add_new_table(tab_id, form_id, repository_name, table_name, json.loads(tab_boundaries))
@@ -48,7 +49,8 @@ def put(session, _id: str, repository: str):
@rt(Routes.ShowTable) @rt(Routes.ShowTable)
def get(session, _id: str, repository: str, table: str, tab_boundaries:str): def get(session, _id: str, repository: str, table: str, tab_boundaries: str):
logger.debug(f"Entering {Routes.ShowTable} with args {debug_session(session)}, {_id=}, {repository=}, {table=}, {tab_boundaries=}") logger.debug(
f"Entering {Routes.ShowTable} with args {debug_session(session)}, {_id=}, {repository=}, {table=}, {tab_boundaries=}")
instance = InstanceManager.get(session, _id) instance = InstanceManager.get(session, _id)
return instance.show_table(repository, table, json.loads(tab_boundaries)) return instance.show_table(repository, table, json.loads(tab_boundaries))

View File

@@ -26,12 +26,12 @@ def to_html(item):
class MyFt: class MyFt:
def __init__(self, name, *args, **kwargs): def __init__(self, name, *args, **kwargs):
self.name = name self.name = name
self.args = args self.children = args
self.kwargs = kwargs self.attrs = kwargs
def to_html(self): def to_html(self):
body_items = [to_html(item) for item in self.args] body_items = [to_html(item) for item in self.children]
return f"<{self.name} {' '.join(f'{attr_map.get(k, k)}="{v}"' for k, v in self.kwargs.items())}>{' '.join(body_items)}</div>" return f"<{self.name} {' '.join(f'{attr_map.get(k, k)}="{v}"' for k, v in self.attrs.items())}>{' '.join(body_items)}</div>"
def __ft__(self): def __ft__(self):
return NotStr(self.to_html()) return NotStr(self.to_html())

View File

@@ -1,6 +1,7 @@
import ast import ast
import base64 import base64
import cProfile import cProfile
import functools
import hashlib import hashlib
import importlib import importlib
import inspect import inspect
@@ -424,6 +425,7 @@ def split_host_port(url):
def timed(func): def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
start = time.perf_counter() start = time.perf_counter()
result = func(*args, **kwargs) result = func(*args, **kwargs)
@@ -449,11 +451,14 @@ def timed(func):
def profile_function(func): def profile_function(func):
@functools.wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
profiler = cProfile.Profile() profiler = cProfile.Profile()
profiler.enable() try:
result = func(*args, **kwargs) profiler.enable()
profiler.disable() result = func(*args, **kwargs)
finally:
profiler.disable()
# Determine class name if any # Determine class name if any
class_name = None class_name = None
@@ -522,4 +527,4 @@ class UnreferencedNamesVisitor(ast.NodeVisitor):
:rtype: :rtype:
""" """
self.names.add(node.arg) self.names.add(node.arg)
self.visit_selected(node, ["value"]) self.visit_selected(node, ["value"])

View File

@@ -1,5 +1,4 @@
# global layout # global layout
import asyncio
import logging.config import logging.config
import random import random
from asyncio import sleep from asyncio import sleep
@@ -216,21 +215,23 @@ app, rt = fast_app(
pico=False, pico=False,
) )
# ------------------------- # -------------------------
# Profiling middleware # Profiling middleware
# ------------------------- # -------------------------
# @app.middleware("http") @app.middleware("http")
# async def timing_middleware(request, call_next): async def timing_middleware(request, call_next):
# start_total = time.perf_counter() import time
# start_total = time.perf_counter()
# # Call the next middleware or route handler
# response = await call_next(request) # Call the next middleware or route handler
# response = await call_next(request)
# end_total = time.perf_counter()
# elapsed = end_total - start_total end_total = time.perf_counter()
# elapsed = end_total - start_total
# print(f"[PERF] Total server time: {elapsed:.4f} sec - Path: {request.url.path}")
# return response print(f"[PERF] Total server time: {elapsed:.4f} sec - Path: {request.url.path}")
return response
settings_manager = SettingsManager() settings_manager = SettingsManager()
@@ -279,7 +280,7 @@ shutdown_event = signal_shutdown()
async def number_generator(): async def number_generator():
while True: # not shutdown_event.is_set(): while True: # not shutdown_event.is_set():
data = Article(random.randint(1, 100)) data = Article(random.randint(1, 100))
print(data) print(data)
yield sse_message(data) yield sse_message(data)
@@ -333,7 +334,7 @@ def not_found(path: str, session=None):
setup_toasts(app) setup_toasts(app)
async def main(): def main():
logger.info(f" Starting FastHTML server on http://localhost:{APP_PORT}") logger.info(f" Starting FastHTML server on http://localhost:{APP_PORT}")
serve(port=APP_PORT) serve(port=APP_PORT)
@@ -341,9 +342,4 @@ async def main():
if __name__ == "__main__": if __name__ == "__main__":
# Start your application # Start your application
logger.info("Application starting...") logger.info("Application starting...")
try: main()
asyncio.run(main())
except KeyboardInterrupt:
logger.info("\nStopping application...")
except Exception as e:
logger.error(f"Error: {e}")