profiling-explorer 1.0.0__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- profiling_explorer-1.1.0/CHANGELOG.rst +29 -0
- {profiling_explorer-1.0.0/src/profiling_explorer.egg-info → profiling_explorer-1.1.0}/PKG-INFO +7 -2
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/README.rst +6 -1
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/pyproject.toml +14 -17
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/main.py +1 -1
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/static/script.js +52 -21
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/views.py +62 -53
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0/src/profiling_explorer.egg-info}/PKG-INFO +7 -2
- profiling_explorer-1.0.0/CHANGELOG.rst +0 -8
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/LICENSE +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/MANIFEST.in +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/setup.cfg +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/__init__.py +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/__main__.py +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/management/__init__.py +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/management/commands/__init__.py +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/management/commands/runserver.py +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/py.typed +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/settings.py +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/static/styles.css +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/templates/_row.html +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/templates/base.html +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/templates/callers_callees.html +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/templates/index.html +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/templatetags/__init__.py +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/templatetags/profiling_explorer_tags.py +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/urls.py +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer.egg-info/SOURCES.txt +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer.egg-info/dependency_links.txt +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer.egg-info/entry_points.txt +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer.egg-info/requires.txt +0 -0
- {profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
=========
|
|
2
|
+
Changelog
|
|
3
|
+
=========
|
|
4
|
+
|
|
5
|
+
1.1.0 (2026-04-13)
|
|
6
|
+
------------------
|
|
7
|
+
|
|
8
|
+
* Extend search to support multiple terms.
|
|
9
|
+
|
|
10
|
+
`PR #33 <https://github.com/adamchainz/profiling-explorer/pull/33>`__.
|
|
11
|
+
|
|
12
|
+
* Add keyboard shortcuts to focus the search box.
|
|
13
|
+
|
|
14
|
+
`PR #35 <https://github.com/adamchainz/profiling-explorer/pull/35>`__.
|
|
15
|
+
|
|
16
|
+
* Fix callers/callees views for built-in functions.
|
|
17
|
+
|
|
18
|
+
`PR #34 <https://github.com/adamchainz/profiling-explorer/pull/34>`__.
|
|
19
|
+
Thanks to David Szotten for the report in `Issue #32 <https://github.com/adamchainz/profiling-explorer/issues/32>`__.
|
|
20
|
+
|
|
21
|
+
1.0.1 (2026-04-02)
|
|
22
|
+
------------------
|
|
23
|
+
|
|
24
|
+
* Fix crash when a record has an empty filename.
|
|
25
|
+
|
|
26
|
+
1.0.0 (2026-04-02)
|
|
27
|
+
------------------
|
|
28
|
+
|
|
29
|
+
* Initial release.
|
{profiling_explorer-1.0.0/src/profiling_explorer.egg-info → profiling_explorer-1.1.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: profiling-explorer
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Table-based exploration tool for Python profiling data (pstats files).
|
|
5
5
|
Author-email: Adam Johnson <me@adamj.eu>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -55,9 +55,11 @@ Table-based exploration tool for Python profiling data (pstats files).
|
|
|
55
55
|
.. …then in another tab:
|
|
56
56
|
.. uvx shot-scraper --width 1280 --height 720 --retina --wait 1000 http://127.0.0.1:8099/ -o screenshot.png
|
|
57
57
|
|
|
58
|
-
.. figure:: https://raw.githubusercontent.com/adamchainz/profiling-explorer/
|
|
58
|
+
.. figure:: https://raw.githubusercontent.com/adamchainz/profiling-explorer/main/screenshot.png
|
|
59
59
|
:alt: profiling-explorer screenshot
|
|
60
60
|
|
|
61
|
+
Read more in `the introductory blog post <https://adamj.eu/tech/2026/04/03/python-introducing-profiling-explorer/>`__.
|
|
62
|
+
|
|
61
63
|
----
|
|
62
64
|
|
|
63
65
|
**Get better at command line Git** with my book `Boost Your Git DX <https://adamchainz.gumroad.com/l/bygdx>`__.
|
|
@@ -110,6 +112,9 @@ Features:
|
|
|
110
112
|
|
|
111
113
|
* Click the **calls**, **internal ms**, or **cumulative ms** column headers to sort by that column.
|
|
112
114
|
* Use the search box to filter by filename or function name.
|
|
115
|
+
Separate multiple search terms by spaces to match records that contain all terms.
|
|
116
|
+
* Press a keyboard shortcut to focus the search box: ``/``, ``Control+K``, or ``Command+K``.
|
|
117
|
+
Select some text first, like a filename, to pre-fill the search box with that text.
|
|
113
118
|
* Hover by a filename + line number pair to reveal the copy button, which copies the location to your clipboard for faster opening.
|
|
114
119
|
* Click the **callers** or **callees** links on the right of a row to see the callers or callees of that function.
|
|
115
120
|
|
|
@@ -28,9 +28,11 @@ Table-based exploration tool for Python profiling data (pstats files).
|
|
|
28
28
|
.. …then in another tab:
|
|
29
29
|
.. uvx shot-scraper --width 1280 --height 720 --retina --wait 1000 http://127.0.0.1:8099/ -o screenshot.png
|
|
30
30
|
|
|
31
|
-
.. figure:: https://raw.githubusercontent.com/adamchainz/profiling-explorer/
|
|
31
|
+
.. figure:: https://raw.githubusercontent.com/adamchainz/profiling-explorer/main/screenshot.png
|
|
32
32
|
:alt: profiling-explorer screenshot
|
|
33
33
|
|
|
34
|
+
Read more in `the introductory blog post <https://adamj.eu/tech/2026/04/03/python-introducing-profiling-explorer/>`__.
|
|
35
|
+
|
|
34
36
|
----
|
|
35
37
|
|
|
36
38
|
**Get better at command line Git** with my book `Boost Your Git DX <https://adamchainz.gumroad.com/l/bygdx>`__.
|
|
@@ -83,6 +85,9 @@ Features:
|
|
|
83
85
|
|
|
84
86
|
* Click the **calls**, **internal ms**, or **cumulative ms** column headers to sort by that column.
|
|
85
87
|
* Use the search box to filter by filename or function name.
|
|
88
|
+
Separate multiple search terms by spaces to match records that contain all terms.
|
|
89
|
+
* Press a keyboard shortcut to focus the search box: ``/``, ``Control+K``, or ``Command+K``.
|
|
90
|
+
Select some text first, like a filename, to pre-fill the search box with that text.
|
|
86
91
|
* Hover by a filename + line number pair to reveal the copy button, which copies the location to your clipboard for faster opening.
|
|
87
92
|
* Click the **callers** or **callees** links on the right of a row to see the callers or callees of that function.
|
|
88
93
|
|
|
@@ -6,7 +6,7 @@ requires = [
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "profiling-explorer"
|
|
9
|
-
version = "1.
|
|
9
|
+
version = "1.1.0"
|
|
10
10
|
description = "Table-based exploration tool for Python profiling data (pstats files)."
|
|
11
11
|
readme = "README.rst"
|
|
12
12
|
keywords = [
|
|
@@ -44,7 +44,7 @@ scripts.profiling-explorer = "profiling_explorer.main:main"
|
|
|
44
44
|
[dependency-groups]
|
|
45
45
|
test = [
|
|
46
46
|
"cogapp>=3.6",
|
|
47
|
-
"coverage[toml]",
|
|
47
|
+
"coverage[toml]>=7.10",
|
|
48
48
|
"pytest",
|
|
49
49
|
"pytest-django",
|
|
50
50
|
"pytest-randomly",
|
|
@@ -90,6 +90,18 @@ lint.isort.required-imports = [ "from __future__ import annotations" ]
|
|
|
90
90
|
[tool.pyproject-fmt]
|
|
91
91
|
max_supported_python = "3.14"
|
|
92
92
|
|
|
93
|
+
[tool.mypy]
|
|
94
|
+
enable_error_code = [
|
|
95
|
+
"ignore-without-code",
|
|
96
|
+
"redundant-expr",
|
|
97
|
+
"truthy-bool",
|
|
98
|
+
]
|
|
99
|
+
mypy_path = "src/"
|
|
100
|
+
namespace_packages = false
|
|
101
|
+
strict = true
|
|
102
|
+
warn_unreachable = true
|
|
103
|
+
overrides = [ { module = "tests.*", allow_untyped_defs = true } ]
|
|
104
|
+
|
|
93
105
|
[tool.pytest]
|
|
94
106
|
ini_options.addopts = """\
|
|
95
107
|
--strict-config
|
|
@@ -111,24 +123,9 @@ paths.source = [
|
|
|
111
123
|
"src",
|
|
112
124
|
".tox/**/site-packages",
|
|
113
125
|
]
|
|
114
|
-
report.exclude_also = [
|
|
115
|
-
"if TYPE_CHECKING:",
|
|
116
|
-
]
|
|
117
126
|
report.show_missing = true
|
|
118
127
|
report.skip_covered = true
|
|
119
128
|
report.skip_empty = true
|
|
120
129
|
|
|
121
|
-
[tool.mypy]
|
|
122
|
-
enable_error_code = [
|
|
123
|
-
"ignore-without-code",
|
|
124
|
-
"redundant-expr",
|
|
125
|
-
"truthy-bool",
|
|
126
|
-
]
|
|
127
|
-
mypy_path = "src/"
|
|
128
|
-
namespace_packages = false
|
|
129
|
-
strict = true
|
|
130
|
-
warn_unreachable = true
|
|
131
|
-
overrides = [ { module = "tests.*", allow_untyped_defs = true } ]
|
|
132
|
-
|
|
133
130
|
[tool.rstcheck]
|
|
134
131
|
report_level = "ERROR"
|
|
@@ -17,7 +17,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
17
17
|
argv = sys.argv[1:]
|
|
18
18
|
|
|
19
19
|
parser = argparse.ArgumentParser(prog="profiling-explorer", allow_abbrev=False)
|
|
20
|
-
parser.suggest_on_error = True
|
|
20
|
+
parser.suggest_on_error = True
|
|
21
21
|
parser.add_argument(
|
|
22
22
|
"filename",
|
|
23
23
|
metavar="FILE",
|
{profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/static/script.js
RENAMED
|
@@ -6,32 +6,63 @@ async function fetchDoc(url, options) {
|
|
|
6
6
|
|
|
7
7
|
// Filter box
|
|
8
8
|
const searchInput = document.getElementById('pe-search');
|
|
9
|
-
let searchTimeout = null;
|
|
10
|
-
let searchAbort = null;
|
|
11
9
|
|
|
10
|
+
document.addEventListener('keydown', (e) => {
|
|
11
|
+
const target = event.composedPath()?.[0] || event.target;
|
|
12
|
+
const isTextField =
|
|
13
|
+
target instanceof HTMLElement &&
|
|
14
|
+
(["TEXTAREA", "INPUT"].includes(target.tagName) ||
|
|
15
|
+
target.isContentEditable);
|
|
16
|
+
|
|
17
|
+
if (isTextField) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const selection = globalThis.getSelection()?.toString();
|
|
22
|
+
const keyPressed = event.key;
|
|
23
|
+
const ctrlOrMetaPressed = event.ctrlKey || event.metaKey;
|
|
24
|
+
const isSlash = keyPressed === "/" && !ctrlOrMetaPressed;
|
|
25
|
+
const isCtrlK = keyPressed === "k" && ctrlOrMetaPressed && !event.shiftKey;
|
|
26
|
+
|
|
27
|
+
if (isSlash || isCtrlK) {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
searchInput.focus();
|
|
30
|
+
if (selection) {
|
|
31
|
+
searchInput.value = selection;
|
|
32
|
+
clearTimeout(searchTimeout);
|
|
33
|
+
searchTimeout = setTimeout(doSearch, 0);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let searchTimeout = null;
|
|
12
39
|
searchInput.addEventListener('input', () => {
|
|
13
40
|
clearTimeout(searchTimeout);
|
|
14
|
-
searchTimeout = setTimeout(
|
|
15
|
-
const q = searchInput.value.trim();
|
|
16
|
-
const url = new URL(window.location.href);
|
|
17
|
-
if (q) {
|
|
18
|
-
url.searchParams.set('q', q);
|
|
19
|
-
} else {
|
|
20
|
-
url.searchParams.delete('q');
|
|
21
|
-
}
|
|
22
|
-
history.replaceState(null, '', url);
|
|
23
|
-
|
|
24
|
-
searchAbort?.abort();
|
|
25
|
-
searchAbort = new AbortController();
|
|
26
|
-
try {
|
|
27
|
-
const doc = await fetchDoc(url, { signal: searchAbort.signal });
|
|
28
|
-
document.querySelector('main').replaceWith(doc.querySelector('main'));
|
|
29
|
-
} catch (e) {
|
|
30
|
-
if (e.name !== 'AbortError') throw e;
|
|
31
|
-
}
|
|
32
|
-
}, 300);
|
|
41
|
+
searchTimeout = setTimeout(doSearch, 300);
|
|
33
42
|
});
|
|
34
43
|
|
|
44
|
+
let searchAbort = null;
|
|
45
|
+
|
|
46
|
+
async function doSearch() {
|
|
47
|
+
const q = searchInput.value.trim();
|
|
48
|
+
const url = new URL(window.location.href);
|
|
49
|
+
if (q) {
|
|
50
|
+
url.searchParams.set('q', q);
|
|
51
|
+
} else {
|
|
52
|
+
url.searchParams.delete('q');
|
|
53
|
+
}
|
|
54
|
+
history.replaceState(null, '', url);
|
|
55
|
+
|
|
56
|
+
searchAbort?.abort();
|
|
57
|
+
searchAbort = new AbortController();
|
|
58
|
+
try {
|
|
59
|
+
const doc = await fetchDoc(url, { signal: searchAbort.signal });
|
|
60
|
+
document.querySelector('main').replaceWith(doc.querySelector('main'));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if (e.name !== 'AbortError') throw e;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
35
66
|
// Copy buttons
|
|
36
67
|
document.addEventListener('click', (e) => {
|
|
37
68
|
const btn = e.target.closest('button.copy-btn');
|
|
@@ -4,6 +4,7 @@ import hashlib
|
|
|
4
4
|
import os
|
|
5
5
|
import pstats
|
|
6
6
|
import re
|
|
7
|
+
from collections import defaultdict
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from importlib.resources import files as resource_files
|
|
9
10
|
from typing import Any
|
|
@@ -23,11 +24,6 @@ class RowStats:
|
|
|
23
24
|
cumulative_ms_pct: float
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
@dataclass(slots=True)
|
|
27
|
-
class EdgeStats(RowStats):
|
|
28
|
-
pass
|
|
29
|
-
|
|
30
|
-
|
|
31
27
|
@dataclass(slots=True)
|
|
32
28
|
class Row(RowStats):
|
|
33
29
|
id: str
|
|
@@ -44,8 +40,8 @@ class Profile:
|
|
|
44
40
|
total_time_ms: int
|
|
45
41
|
rows: list[Row]
|
|
46
42
|
rows_by_id: dict[str, Row]
|
|
47
|
-
callers_map:
|
|
48
|
-
callees_map:
|
|
43
|
+
callers_map: defaultdict[str, dict[str, RowStats]]
|
|
44
|
+
callees_map: defaultdict[str, dict[str, RowStats]]
|
|
49
45
|
|
|
50
46
|
|
|
51
47
|
# Populated by main()
|
|
@@ -68,11 +64,21 @@ _STRIP_PREFIX_RE = re.compile(
|
|
|
68
64
|
)
|
|
69
65
|
|
|
70
66
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
def _shorten_filename_function(filename: str, funcname: str) -> tuple[str, str, str]:
|
|
68
|
+
if filename == "~":
|
|
69
|
+
if funcname.startswith("<") and funcname.endswith(">"):
|
|
70
|
+
funcname = f"{{{funcname[1:-1]}}}"
|
|
71
|
+
short_filename = ""
|
|
72
|
+
full_filename = ""
|
|
73
|
+
else:
|
|
74
|
+
if filename == "":
|
|
75
|
+
short_filename = ""
|
|
76
|
+
match = _STRIP_PREFIX_RE.match(filename)
|
|
77
|
+
if match:
|
|
78
|
+
short_filename = filename[match.end() :]
|
|
79
|
+
short_filename = os.path.relpath(filename)
|
|
80
|
+
full_filename = filename
|
|
81
|
+
return full_filename, short_filename, funcname
|
|
76
82
|
|
|
77
83
|
|
|
78
84
|
def _row_id(full_filename: str, lineno: int, funcname: str) -> str:
|
|
@@ -93,17 +99,14 @@ def build_profile(s: pstats.Stats, path: str) -> Profile:
|
|
|
93
99
|
total_time_ms = round(s.total_tt * 1000) # type: ignore[attr-defined]
|
|
94
100
|
rows = []
|
|
95
101
|
rows_by_id = {}
|
|
102
|
+
callers_map: defaultdict[str, dict[str, RowStats]] = defaultdict(dict)
|
|
103
|
+
callees_map: defaultdict[str, dict[str, RowStats]] = defaultdict(dict)
|
|
96
104
|
for key in s.fcn_list: # type: ignore[attr-defined]
|
|
97
105
|
filename, lineno, funcname = key
|
|
98
|
-
_, calls, tottime, cumtime,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
short_filename = ""
|
|
103
|
-
full_filename = ""
|
|
104
|
-
else:
|
|
105
|
-
short_filename = _shorten_filename(filename)
|
|
106
|
-
full_filename = filename
|
|
106
|
+
_, calls, tottime, cumtime, callers = s.stats[key] # type: ignore[attr-defined]
|
|
107
|
+
full_filename, short_filename, funcname = _shorten_filename_function(
|
|
108
|
+
filename, funcname
|
|
109
|
+
)
|
|
107
110
|
row_id = _row_id(full_filename, lineno, funcname)
|
|
108
111
|
cumulative_ms = round(cumtime * 1_000)
|
|
109
112
|
row = Row(
|
|
@@ -122,31 +125,32 @@ def build_profile(s: pstats.Stats, path: str) -> Profile:
|
|
|
122
125
|
)
|
|
123
126
|
rows.append(row)
|
|
124
127
|
rows_by_id[row_id] = row
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
128
|
+
for (caller_filename, caller_lineno, caller_funcname), (
|
|
129
|
+
callee_calls,
|
|
130
|
+
_,
|
|
131
|
+
__,
|
|
132
|
+
callee_cumtime,
|
|
133
|
+
) in callers.items():
|
|
134
|
+
caller_filename, _, caller_funcname = _shorten_filename_function(
|
|
135
|
+
caller_filename, caller_funcname
|
|
136
|
+
)
|
|
137
|
+
caller_id = _row_id(caller_filename, caller_lineno, caller_funcname)
|
|
138
|
+
callee_cumulative_ms = round(callee_cumtime * 1_000)
|
|
139
|
+
edge = RowStats(
|
|
140
|
+
calls=callee_calls,
|
|
141
|
+
calls_pct=min(100.0, callee_calls / total_calls * 100)
|
|
142
|
+
if total_calls
|
|
143
|
+
else 0.0,
|
|
144
|
+
internal_ms=None,
|
|
145
|
+
cumulative_ms=callee_cumulative_ms,
|
|
146
|
+
cumulative_ms_pct=(
|
|
147
|
+
min(100.0, cumulative_ms / total_time_ms * 100)
|
|
148
|
+
if total_time_ms
|
|
149
|
+
else 0.0
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
callers_map[row_id][caller_id] = edge
|
|
153
|
+
callees_map[caller_id][row_id] = edge
|
|
150
154
|
|
|
151
155
|
return Profile(
|
|
152
156
|
filename=path,
|
|
@@ -160,8 +164,8 @@ def build_profile(s: pstats.Stats, path: str) -> Profile:
|
|
|
160
164
|
|
|
161
165
|
|
|
162
166
|
def _build_edge_rows(
|
|
163
|
-
edges: dict[str,
|
|
164
|
-
) -> list[tuple[Row,
|
|
167
|
+
edges: dict[str, RowStats],
|
|
168
|
+
) -> list[tuple[Row, RowStats]]:
|
|
165
169
|
result = [
|
|
166
170
|
(row, edge)
|
|
167
171
|
for row_id, edge in edges.items()
|
|
@@ -181,7 +185,7 @@ def _render_table(
|
|
|
181
185
|
rows: list[Row],
|
|
182
186
|
template: str,
|
|
183
187
|
extra_context: dict[str, Any],
|
|
184
|
-
edge_stats: dict[str,
|
|
188
|
+
edge_stats: dict[str, RowStats] | None = None,
|
|
185
189
|
) -> HttpResponse:
|
|
186
190
|
sort_param = request.GET.get("sort", "-cumulative_ms")
|
|
187
191
|
sort_desc = not sort_param.startswith("+")
|
|
@@ -192,9 +196,14 @@ def _render_table(
|
|
|
192
196
|
sort_param = "-cumulative_ms"
|
|
193
197
|
|
|
194
198
|
q = request.GET.get("q", "").strip()
|
|
199
|
+
queries = [query.strip() for query in q.split() if q.strip()]
|
|
195
200
|
filtered_rows = rows
|
|
196
|
-
if
|
|
197
|
-
filtered_rows = [
|
|
201
|
+
if queries:
|
|
202
|
+
filtered_rows = [
|
|
203
|
+
r
|
|
204
|
+
for r in rows
|
|
205
|
+
if all(query in r.filename or query in r.funcname for query in queries)
|
|
206
|
+
]
|
|
198
207
|
|
|
199
208
|
if edge_stats is None:
|
|
200
209
|
sorted_rows = sorted(
|
|
@@ -255,14 +264,14 @@ def index(request: HttpRequest) -> HttpResponse:
|
|
|
255
264
|
def _callers_callees_view(
|
|
256
265
|
request: HttpRequest,
|
|
257
266
|
focal_row: Row,
|
|
258
|
-
edges: dict[str,
|
|
267
|
+
edges: dict[str, RowStats],
|
|
259
268
|
opposite_url: str,
|
|
260
269
|
heading: str,
|
|
261
270
|
opposite_label: str,
|
|
262
271
|
) -> HttpResponse:
|
|
263
272
|
rows_with_edges = _build_edge_rows(edges)
|
|
264
273
|
rows = [r for r, _ in rows_with_edges]
|
|
265
|
-
edge_stats: dict[str,
|
|
274
|
+
edge_stats: dict[str, RowStats] = {r.id: e for r, e in rows_with_edges}
|
|
266
275
|
|
|
267
276
|
return _render_table(
|
|
268
277
|
request,
|
{profiling_explorer-1.0.0 → profiling_explorer-1.1.0/src/profiling_explorer.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: profiling-explorer
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Table-based exploration tool for Python profiling data (pstats files).
|
|
5
5
|
Author-email: Adam Johnson <me@adamj.eu>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -55,9 +55,11 @@ Table-based exploration tool for Python profiling data (pstats files).
|
|
|
55
55
|
.. …then in another tab:
|
|
56
56
|
.. uvx shot-scraper --width 1280 --height 720 --retina --wait 1000 http://127.0.0.1:8099/ -o screenshot.png
|
|
57
57
|
|
|
58
|
-
.. figure:: https://raw.githubusercontent.com/adamchainz/profiling-explorer/
|
|
58
|
+
.. figure:: https://raw.githubusercontent.com/adamchainz/profiling-explorer/main/screenshot.png
|
|
59
59
|
:alt: profiling-explorer screenshot
|
|
60
60
|
|
|
61
|
+
Read more in `the introductory blog post <https://adamj.eu/tech/2026/04/03/python-introducing-profiling-explorer/>`__.
|
|
62
|
+
|
|
61
63
|
----
|
|
62
64
|
|
|
63
65
|
**Get better at command line Git** with my book `Boost Your Git DX <https://adamchainz.gumroad.com/l/bygdx>`__.
|
|
@@ -110,6 +112,9 @@ Features:
|
|
|
110
112
|
|
|
111
113
|
* Click the **calls**, **internal ms**, or **cumulative ms** column headers to sort by that column.
|
|
112
114
|
* Use the search box to filter by filename or function name.
|
|
115
|
+
Separate multiple search terms by spaces to match records that contain all terms.
|
|
116
|
+
* Press a keyboard shortcut to focus the search box: ``/``, ``Control+K``, or ``Command+K``.
|
|
117
|
+
Select some text first, like a filename, to pre-fill the search box with that text.
|
|
113
118
|
* Hover by a filename + line number pair to reveal the copy button, which copies the location to your clipboard for faster opening.
|
|
114
119
|
* Click the **callers** or **callees** links on the right of a row to see the callers or callees of that function.
|
|
115
120
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/management/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/static/styles.css
RENAMED
|
File without changes
|
{profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/templates/_row.html
RENAMED
|
File without changes
|
{profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/templates/base.html
RENAMED
|
File without changes
|
|
File without changes
|
{profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer/templates/index.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer.egg-info/requires.txt
RENAMED
|
File without changes
|
{profiling_explorer-1.0.0 → profiling_explorer-1.1.0}/src/profiling_explorer.egg-info/top_level.txt
RENAMED
|
File without changes
|