mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
perf: Add detailed performance logging to model retrieval, filtering, and sorting operations. see #711
This commit is contained in:
@@ -5,6 +5,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Awaitable, Callable, Dict, Iterable, List, Mapping, Optional
|
from typing import Any, Awaitable, Callable, Dict, Iterable, List, Mapping, Optional
|
||||||
|
|
||||||
@@ -160,9 +161,12 @@ class ModelListingHandler:
|
|||||||
self._logger = logger
|
self._logger = logger
|
||||||
|
|
||||||
async def get_models(self, request: web.Request) -> web.Response:
|
async def get_models(self, request: web.Request) -> web.Response:
|
||||||
|
start_time = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
params = self._parse_common_params(request)
|
params = self._parse_common_params(request)
|
||||||
result = await self._service.get_paginated_data(**params)
|
result = await self._service.get_paginated_data(**params)
|
||||||
|
|
||||||
|
format_start = time.perf_counter()
|
||||||
formatted_result = {
|
formatted_result = {
|
||||||
"items": [await self._service.format_response(item) for item in result["items"]],
|
"items": [await self._service.format_response(item) for item in result["items"]],
|
||||||
"total": result["total"],
|
"total": result["total"],
|
||||||
@@ -170,6 +174,13 @@ class ModelListingHandler:
|
|||||||
"page_size": result["page_size"],
|
"page_size": result["page_size"],
|
||||||
"total_pages": result["total_pages"],
|
"total_pages": result["total_pages"],
|
||||||
}
|
}
|
||||||
|
format_duration = time.perf_counter() - format_start
|
||||||
|
|
||||||
|
duration = time.perf_counter() - start_time
|
||||||
|
self._logger.info(
|
||||||
|
"Request for %s/list took %.3fs (formatting: %.3fs)",
|
||||||
|
self._service.model_type, duration, format_duration
|
||||||
|
)
|
||||||
return web.json_response(formatted_result)
|
return web.json_response(formatted_result)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._logger.error("Error retrieving %ss: %s", self._service.model_type, exc, exc_info=True)
|
self._logger.error("Error retrieving %ss: %s", self._service.model_type, exc, exc_info=True)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import asyncio
|
|||||||
from typing import Any, Dict, List, Optional, Type, TYPE_CHECKING
|
from typing import Any, Dict, List, Optional, Type, TYPE_CHECKING
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
from ..utils.constants import VALID_LORA_TYPES
|
from ..utils.constants import VALID_LORA_TYPES
|
||||||
from ..utils.models import BaseModelMetadata
|
from ..utils.models import BaseModelMetadata
|
||||||
@@ -79,10 +80,16 @@ class BaseModelService(ABC):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
"""Get paginated and filtered model data"""
|
"""Get paginated and filtered model data"""
|
||||||
|
overall_start = time.perf_counter()
|
||||||
|
|
||||||
sort_params = self.cache_repository.parse_sort(sort_by)
|
sort_params = self.cache_repository.parse_sort(sort_by)
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
sorted_data = await self.cache_repository.fetch_sorted(sort_params)
|
sorted_data = await self.cache_repository.fetch_sorted(sort_params)
|
||||||
|
fetch_duration = time.perf_counter() - t0
|
||||||
|
initial_count = len(sorted_data)
|
||||||
|
|
||||||
|
t1 = time.perf_counter()
|
||||||
if hash_filters:
|
if hash_filters:
|
||||||
filtered_data = await self._apply_hash_filters(sorted_data, hash_filters)
|
filtered_data = await self._apply_hash_filters(sorted_data, hash_filters)
|
||||||
else:
|
else:
|
||||||
@@ -112,17 +119,25 @@ class BaseModelService(ABC):
|
|||||||
|
|
||||||
if allow_selling_generated_content is not None:
|
if allow_selling_generated_content is not None:
|
||||||
filtered_data = await self._apply_allow_selling_filter(filtered_data, allow_selling_generated_content)
|
filtered_data = await self._apply_allow_selling_filter(filtered_data, allow_selling_generated_content)
|
||||||
|
filter_duration = time.perf_counter() - t1
|
||||||
|
post_filter_count = len(filtered_data)
|
||||||
|
|
||||||
annotated_for_filter: Optional[List[Dict]] = None
|
annotated_for_filter: Optional[List[Dict]] = None
|
||||||
|
t2 = time.perf_counter()
|
||||||
if update_available_only:
|
if update_available_only:
|
||||||
annotated_for_filter = await self._annotate_update_flags(filtered_data)
|
annotated_for_filter = await self._annotate_update_flags(filtered_data)
|
||||||
filtered_data = [
|
filtered_data = [
|
||||||
item for item in annotated_for_filter
|
item for item in annotated_for_filter
|
||||||
if item.get('update_available')
|
if item.get('update_available')
|
||||||
]
|
]
|
||||||
|
update_filter_duration = time.perf_counter() - t2
|
||||||
|
final_count = len(filtered_data)
|
||||||
|
|
||||||
|
t3 = time.perf_counter()
|
||||||
paginated = self._paginate(filtered_data, page, page_size)
|
paginated = self._paginate(filtered_data, page, page_size)
|
||||||
|
pagination_duration = time.perf_counter() - t3
|
||||||
|
|
||||||
|
t4 = time.perf_counter()
|
||||||
if update_available_only:
|
if update_available_only:
|
||||||
# Items already include update flags thanks to the pre-filter annotation.
|
# Items already include update flags thanks to the pre-filter annotation.
|
||||||
paginated['items'] = list(paginated['items'])
|
paginated['items'] = list(paginated['items'])
|
||||||
@@ -130,6 +145,16 @@ class BaseModelService(ABC):
|
|||||||
paginated['items'] = await self._annotate_update_flags(
|
paginated['items'] = await self._annotate_update_flags(
|
||||||
paginated['items'],
|
paginated['items'],
|
||||||
)
|
)
|
||||||
|
annotate_duration = time.perf_counter() - t4
|
||||||
|
|
||||||
|
overall_duration = time.perf_counter() - overall_start
|
||||||
|
logger.info(
|
||||||
|
"%s.get_paginated_data took %.3fs (fetch: %.3fs, filter: %.3fs, update_filter: %.3fs, pagination: %.3fs, annotate: %.3fs). "
|
||||||
|
"Counts: initial=%d, post_filter=%d, final=%d",
|
||||||
|
self.__class__.__name__, overall_duration, fetch_duration, filter_duration,
|
||||||
|
update_filter_duration, pagination_duration, annotate_duration,
|
||||||
|
initial_count, post_filter_count, final_count
|
||||||
|
)
|
||||||
return paginated
|
return paginated
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
@@ -212,40 +216,53 @@ class ModelCache:
|
|||||||
|
|
||||||
def _sort_data(self, data: List[Dict], sort_key: str, order: str) -> List[Dict]:
|
def _sort_data(self, data: List[Dict], sort_key: str, order: str) -> List[Dict]:
|
||||||
"""Sort data by sort_key and order"""
|
"""Sort data by sort_key and order"""
|
||||||
|
start_time = time.perf_counter()
|
||||||
reverse = (order == 'desc')
|
reverse = (order == 'desc')
|
||||||
if sort_key == 'name':
|
if sort_key == 'name':
|
||||||
# Natural sort by configured display name, case-insensitive
|
# Natural sort by configured display name, case-insensitive
|
||||||
return natsorted(
|
result = natsorted(
|
||||||
data,
|
data,
|
||||||
key=lambda x: self._get_display_name(x).lower(),
|
key=lambda x: self._get_display_name(x).lower(),
|
||||||
reverse=reverse
|
reverse=reverse
|
||||||
)
|
)
|
||||||
elif sort_key == 'date':
|
elif sort_key == 'date':
|
||||||
# Sort by modified timestamp
|
# Sort by modified timestamp
|
||||||
return sorted(
|
result = sorted(
|
||||||
data,
|
data,
|
||||||
key=itemgetter('modified'),
|
key=itemgetter('modified'),
|
||||||
reverse=reverse
|
reverse=reverse
|
||||||
)
|
)
|
||||||
elif sort_key == 'size':
|
elif sort_key == 'size':
|
||||||
# Sort by file size
|
# Sort by file size
|
||||||
return sorted(
|
result = sorted(
|
||||||
data,
|
data,
|
||||||
key=itemgetter('size'),
|
key=itemgetter('size'),
|
||||||
reverse=reverse
|
reverse=reverse
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Fallback: no sort
|
# Fallback: no sort
|
||||||
return list(data)
|
result = list(data)
|
||||||
|
|
||||||
|
duration = time.perf_counter() - start_time
|
||||||
|
if duration > 0.05:
|
||||||
|
logger.info("ModelCache._sort_data(%s, %s) for %d items took %.3fs", sort_key, order, len(data), duration)
|
||||||
|
return result
|
||||||
|
|
||||||
async def get_sorted_data(self, sort_key: str = 'name', order: str = 'asc') -> List[Dict]:
|
async def get_sorted_data(self, sort_key: str = 'name', order: str = 'asc') -> List[Dict]:
|
||||||
"""Get sorted data by sort_key and order, using cache if possible"""
|
"""Get sorted data by sort_key and order, using cache if possible"""
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
if (sort_key, order) == self._last_sort:
|
if (sort_key, order) == self._last_sort:
|
||||||
return self._last_sorted_data
|
return self._last_sorted_data
|
||||||
|
|
||||||
|
start_time = time.perf_counter()
|
||||||
sorted_data = self._sort_data(self.raw_data, sort_key, order)
|
sorted_data = self._sort_data(self.raw_data, sort_key, order)
|
||||||
self._last_sort = (sort_key, order)
|
self._last_sort = (sort_key, order)
|
||||||
self._last_sorted_data = sorted_data
|
self._last_sorted_data = sorted_data
|
||||||
|
|
||||||
|
duration = time.perf_counter() - start_time
|
||||||
|
if duration > 0.1:
|
||||||
|
logger.debug("ModelCache.get_sorted_data(%s, %s) took %.3fs", sort_key, order, duration)
|
||||||
|
|
||||||
return sorted_data
|
return sorted_data
|
||||||
|
|
||||||
async def update_name_display_mode(self, display_mode: str) -> None:
|
async def update_name_display_mode(self, display_mode: str) -> None:
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|||||||
|
|
||||||
from ..utils.constants import NSFW_LEVELS
|
from ..utils.constants import NSFW_LEVELS
|
||||||
from ..utils.utils import fuzzy_match as default_fuzzy_match
|
from ..utils.utils import fuzzy_match as default_fuzzy_match
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CIVITAI_MODEL_TYPE = "LORA"
|
DEFAULT_CIVITAI_MODEL_TYPE = "LORA"
|
||||||
@@ -115,22 +119,33 @@ class ModelFilterSet:
|
|||||||
|
|
||||||
def apply(self, data: Iterable[Dict[str, Any]], criteria: FilterCriteria) -> List[Dict[str, Any]]:
|
def apply(self, data: Iterable[Dict[str, Any]], criteria: FilterCriteria) -> List[Dict[str, Any]]:
|
||||||
"""Return items that satisfy the provided criteria."""
|
"""Return items that satisfy the provided criteria."""
|
||||||
|
overall_start = time.perf_counter()
|
||||||
items = list(data)
|
items = list(data)
|
||||||
|
initial_count = len(items)
|
||||||
|
|
||||||
if self._settings.get("show_only_sfw", False):
|
if self._settings.get("show_only_sfw", False):
|
||||||
|
t0 = time.perf_counter()
|
||||||
threshold = self._nsfw_levels.get("R", 0)
|
threshold = self._nsfw_levels.get("R", 0)
|
||||||
items = [
|
items = [
|
||||||
item for item in items
|
item for item in items
|
||||||
if not item.get("preview_nsfw_level") or item.get("preview_nsfw_level") < threshold
|
if not item.get("preview_nsfw_level") or item.get("preview_nsfw_level") < threshold
|
||||||
]
|
]
|
||||||
|
sfw_duration = time.perf_counter() - t0
|
||||||
|
else:
|
||||||
|
sfw_duration = 0
|
||||||
|
|
||||||
|
favorites_duration = 0
|
||||||
if criteria.favorites_only:
|
if criteria.favorites_only:
|
||||||
|
t0 = time.perf_counter()
|
||||||
items = [item for item in items if item.get("favorite", False)]
|
items = [item for item in items if item.get("favorite", False)]
|
||||||
|
favorites_duration = time.perf_counter() - t0
|
||||||
|
|
||||||
|
folder_duration = 0
|
||||||
folder = criteria.folder
|
folder = criteria.folder
|
||||||
options = criteria.search_options or {}
|
options = criteria.search_options or {}
|
||||||
recursive = bool(options.get("recursive", True))
|
recursive = bool(options.get("recursive", True))
|
||||||
if folder is not None:
|
if folder is not None:
|
||||||
|
t0 = time.perf_counter()
|
||||||
if recursive:
|
if recursive:
|
||||||
if folder:
|
if folder:
|
||||||
folder_with_sep = f"{folder}/"
|
folder_with_sep = f"{folder}/"
|
||||||
@@ -140,61 +155,82 @@ class ModelFilterSet:
|
|||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
items = [item for item in items if item.get("folder") == folder]
|
items = [item for item in items if item.get("folder") == folder]
|
||||||
|
folder_duration = time.perf_counter() - t0
|
||||||
|
|
||||||
|
base_models_duration = 0
|
||||||
base_models = criteria.base_models or []
|
base_models = criteria.base_models or []
|
||||||
if base_models:
|
if base_models:
|
||||||
|
t0 = time.perf_counter()
|
||||||
base_model_set = set(base_models)
|
base_model_set = set(base_models)
|
||||||
items = [item for item in items if item.get("base_model") in base_model_set]
|
items = [item for item in items if item.get("base_model") in base_model_set]
|
||||||
|
base_models_duration = time.perf_counter() - t0
|
||||||
|
|
||||||
|
tags_duration = 0
|
||||||
tag_filters = criteria.tags or {}
|
tag_filters = criteria.tags or {}
|
||||||
include_tags = set()
|
if tag_filters:
|
||||||
exclude_tags = set()
|
t0 = time.perf_counter()
|
||||||
if isinstance(tag_filters, dict):
|
include_tags = set()
|
||||||
for tag, state in tag_filters.items():
|
exclude_tags = set()
|
||||||
if not tag:
|
if isinstance(tag_filters, dict):
|
||||||
continue
|
for tag, state in tag_filters.items():
|
||||||
if state == "exclude":
|
if not tag:
|
||||||
exclude_tags.add(tag)
|
continue
|
||||||
else:
|
if state == "exclude":
|
||||||
include_tags.add(tag)
|
exclude_tags.add(tag)
|
||||||
else:
|
else:
|
||||||
include_tags = {tag for tag in tag_filters if tag}
|
include_tags.add(tag)
|
||||||
|
else:
|
||||||
|
include_tags = {tag for tag in tag_filters if tag}
|
||||||
|
|
||||||
if include_tags:
|
if include_tags:
|
||||||
def matches_include(item_tags):
|
def matches_include(item_tags):
|
||||||
if not item_tags and "__no_tags__" in include_tags:
|
if not item_tags and "__no_tags__" in include_tags:
|
||||||
return True
|
return True
|
||||||
return any(tag in include_tags for tag in (item_tags or []))
|
return any(tag in include_tags for tag in (item_tags or []))
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
item for item in items
|
item for item in items
|
||||||
if matches_include(item.get("tags"))
|
if matches_include(item.get("tags"))
|
||||||
]
|
]
|
||||||
|
|
||||||
if exclude_tags:
|
if exclude_tags:
|
||||||
def matches_exclude(item_tags):
|
def matches_exclude(item_tags):
|
||||||
if not item_tags and "__no_tags__" in exclude_tags:
|
if not item_tags and "__no_tags__" in exclude_tags:
|
||||||
return True
|
return True
|
||||||
return any(tag in exclude_tags for tag in (item_tags or []))
|
return any(tag in exclude_tags for tag in (item_tags or []))
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
item for item in items
|
item for item in items
|
||||||
if not matches_exclude(item.get("tags"))
|
if not matches_exclude(item.get("tags"))
|
||||||
]
|
]
|
||||||
|
tags_duration = time.perf_counter() - t0
|
||||||
|
|
||||||
|
model_types_duration = 0
|
||||||
model_types = criteria.model_types or []
|
model_types = criteria.model_types or []
|
||||||
normalized_model_types = {
|
if model_types:
|
||||||
model_type for model_type in (
|
t0 = time.perf_counter()
|
||||||
normalize_civitai_model_type(value) for value in model_types
|
normalized_model_types = {
|
||||||
)
|
model_type for model_type in (
|
||||||
if model_type
|
normalize_civitai_model_type(value) for value in model_types
|
||||||
}
|
)
|
||||||
if normalized_model_types:
|
if model_type
|
||||||
items = [
|
}
|
||||||
item for item in items
|
if normalized_model_types:
|
||||||
if normalize_civitai_model_type(resolve_civitai_model_type(item)) in normalized_model_types
|
items = [
|
||||||
]
|
item for item in items
|
||||||
|
if normalize_civitai_model_type(resolve_civitai_model_type(item)) in normalized_model_types
|
||||||
|
]
|
||||||
|
model_types_duration = time.perf_counter() - t0
|
||||||
|
|
||||||
|
duration = time.perf_counter() - overall_start
|
||||||
|
if duration > 0.1: # Only log if it's potentially slow
|
||||||
|
logger.info(
|
||||||
|
"ModelFilterSet.apply took %.3fs (sfw: %.3fs, fav: %.3fs, folder: %.3fs, base: %.3fs, tags: %.3fs, types: %.3fs). "
|
||||||
|
"Count: %d -> %d",
|
||||||
|
duration, sfw_duration, favorites_duration, folder_duration,
|
||||||
|
base_models_duration, tags_duration, model_types_duration,
|
||||||
|
initial_count, len(items)
|
||||||
|
)
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user