@evref-bl/pharo-mcp 1.5.1 → 2.0.0
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.
- package/README.md +18 -13
- package/node/installer.js +9 -7
- package/package.json +1 -1
- package/templates/pharo-references/debugging.md +52 -129
- package/templates/pharo-references/getting-started.md +13 -5
- package/templates/pharo-references/pharo-coding-rules.md +1 -1
- package/templates/pharo-references/pharo-coding-style.md +2 -0
- package/templates/pharo-references/safety-and-ecosystem.md +106 -39
- package/templates/pharo-references/tool-reference.md +78 -207
- package/templates/pharo-references/troubleshooting.md +19 -11
- package/templates/pharo-references/using-pharo-mcp.md +43 -28
- package/templates/pharo-skills/pharo-code-profiling/SKILL.md +205 -0
- package/templates/pharo-skills/pharo-code-profiling/agents/openai.yaml +4 -0
- package/templates/pharo-skills/pharo-image-git-handoff/SKILL.md +3 -0
- package/templates/pharo.AGENTS.md +3 -1
- package/templates/pharo-references/debugger-driven-development.md +0 -101
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pharo-code-profiling
|
|
3
|
+
description: Profile running Pharo code to find where its time goes, and read the sampling report it answers. Use for slow code, before-and-after comparisons, and whenever a single `#timeToRun` number would not say why.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Pharo Code Profiling
|
|
7
|
+
|
|
8
|
+
## Model
|
|
9
|
+
|
|
10
|
+
`#timeToRun` answers how long, never why, and one run is noisy: JIT warm-up, GC
|
|
11
|
+
and scheduling all move it. A sampling profiler interrupts execution every
|
|
12
|
+
millisecond and records the stack, so what it answers is a distribution. Trust
|
|
13
|
+
the ranking and the proportions, not the individual numbers.
|
|
14
|
+
|
|
15
|
+
Two samplers answer text in a headless image, and they differ in what they can
|
|
16
|
+
see, not only in how much:
|
|
17
|
+
|
|
18
|
+
- `MessageTally` is a Smalltalk process that wakes on a delay and records what
|
|
19
|
+
it preempted. Spied on a block, its tree is rooted at that block, so the
|
|
20
|
+
report holds the workload and nothing else. Start here.
|
|
21
|
+
- `AndreasSystemProfiler` is driven by the VM. It samples every process,
|
|
22
|
+
including those running above the priority at which a Smalltalk sampler would
|
|
23
|
+
itself be starved, and it records which primitive the VM is executing, which
|
|
24
|
+
`MessageTally` can only lump together as `primitives`. Its tree is rooted at
|
|
25
|
+
the image.
|
|
26
|
+
|
|
27
|
+
## Workflow
|
|
28
|
+
|
|
29
|
+
1. Build a workload that runs for one to five seconds: long enough for a
|
|
30
|
+
thousand samples, short enough to answer inside the client's timeout, since
|
|
31
|
+
`image_evaluate` has none of its own. Fork a longer one instead, as below.
|
|
32
|
+
Run the workload once before measuring so the JIT is warm.
|
|
33
|
+
2. Profile it and keep the tally, which is what renders the report:
|
|
34
|
+
|
|
35
|
+
```smalltalk
|
|
36
|
+
| workload tally |
|
|
37
|
+
workload := [ 200 timesRepeat: [ "..." ] ].
|
|
38
|
+
workload value.
|
|
39
|
+
tally := MessageTally
|
|
40
|
+
spyOn: workload
|
|
41
|
+
reportOtherProcesses: false
|
|
42
|
+
cutoff: 2
|
|
43
|
+
openResultWindow: false
|
|
44
|
+
closeAfter: false.
|
|
45
|
+
String streamContents: [ :stream | tally report: stream cutoff: 2 ]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
3. Read the report.
|
|
49
|
+
4. Change one thing, re-profile the same workload at the same cutoff, and
|
|
50
|
+
compare shapes. Verify behavior with focused tests.
|
|
51
|
+
|
|
52
|
+
## Choosing The Cutoff
|
|
53
|
+
|
|
54
|
+
The cutoff is the percentage of all samples a node must hold to be printed, in
|
|
55
|
+
the tree and in the leaves alike. Use 2 and read the report once. Over a
|
|
56
|
+
2400-sample run of the same workload it printed 79 lines where 5% printed 52
|
|
57
|
+
and 1% printed 84, so it is close enough to the whole tree that a second look
|
|
58
|
+
rarely pays, and still under a thousand tokens.
|
|
59
|
+
|
|
60
|
+
Raise it when the report is larger than the question. Rendering again costs no
|
|
61
|
+
new run, because the tally is still in hand:
|
|
62
|
+
|
|
63
|
+
```smalltalk
|
|
64
|
+
String streamContents: [ :stream | tally report: stream cutoff: 10 ]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Do not lower it to find a workload whose methods are missing from the report.
|
|
68
|
+
A node printed at cutoff *c* over *n* samples holds `n * c / 100` of them, and
|
|
69
|
+
under about 25 samples that is noise rather than a measurement, so 2% means
|
|
70
|
+
nothing below some 1250 samples. Lengthen the run instead.
|
|
71
|
+
|
|
72
|
+
The `cutoff:` passed to the spy call only feeds the window it is not opening.
|
|
73
|
+
The one that decides the report is on `report:cutoff:`.
|
|
74
|
+
|
|
75
|
+
## Profiling A Workload Longer Than The Call
|
|
76
|
+
|
|
77
|
+
Fork the run, write the report to a file, and come back for it. The fork
|
|
78
|
+
returns the call immediately and leaves the image answering, so the profile can
|
|
79
|
+
take as long as the workload needs. Keep the report out of a global: a file
|
|
80
|
+
survives, is readable from outside the image, and needs no cleanup inside it.
|
|
81
|
+
|
|
82
|
+
```smalltalk
|
|
83
|
+
| path |
|
|
84
|
+
path := FileLocator imageDirectory / 'profile-report.txt'.
|
|
85
|
+
path ensureDelete.
|
|
86
|
+
[
|
|
87
|
+
| tally |
|
|
88
|
+
tally := MessageTally
|
|
89
|
+
spyOn: [ "..." ]
|
|
90
|
+
reportOtherProcesses: false
|
|
91
|
+
cutoff: 2
|
|
92
|
+
openResultWindow: false
|
|
93
|
+
closeAfter: false.
|
|
94
|
+
path writeStreamDo: [ :out |
|
|
95
|
+
out << ((String streamContents: [ :stream | tally report: stream cutoff: 2 ])
|
|
96
|
+
copyReplaceAll: String cr with: String lf) ] ]
|
|
97
|
+
forkAt: Processor userBackgroundPriority
|
|
98
|
+
named: 'pharo-profile'.
|
|
99
|
+
path fullName
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The file is written only once the run ends, so `path exists` answers whether it
|
|
103
|
+
is done. To tell a slow run from a dead one, look for the process by the name
|
|
104
|
+
it was forked under:
|
|
105
|
+
|
|
106
|
+
```smalltalk
|
|
107
|
+
Process allInstances anySatisfy: [ :each |
|
|
108
|
+
each name = 'pharo-profile' and: [ each isTerminated not ] ]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Pharo writes CR line endings, so convert to LF as above, or the report arrives
|
|
112
|
+
as a single line everywhere outside the image.
|
|
113
|
+
|
|
114
|
+
The tally dies with the fork, so this report is written once and cannot be
|
|
115
|
+
rendered again at another cutoff. The file costs nothing to keep, and it can be
|
|
116
|
+
read in parts rather than whole.
|
|
117
|
+
|
|
118
|
+
Forking at `userBackgroundPriority` keeps the image answering calls while the
|
|
119
|
+
profile runs. The samples it loses to those calls are charged to another
|
|
120
|
+
process and dropped from the report, which lowers every percentage by the same
|
|
121
|
+
factor: read the proportions between nodes, not their absolute values.
|
|
122
|
+
|
|
123
|
+
## Reading The Report
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
- 933 tallies, 933 msec.
|
|
127
|
+
|
|
128
|
+
**Tree**
|
|
129
|
+
Process: (60) ...
|
|
130
|
+
63.9% {596ms} Array(SequenceableCollection)>>shuffle
|
|
131
|
+
63.9% {596ms} Array(SequenceableCollection)>>shuffleBy:
|
|
132
|
+
62.7% {585ms} Random>>nextInteger:
|
|
133
|
+
34.2% {319ms} Random>>privateNextValue
|
|
134
|
+
|34.2% {319ms} Random class>>primitiveRandomNumber:
|
|
135
|
+
| 17.8% {166ms} LargePositiveInteger(Integer)>>>>
|
|
136
|
+
| |17.6% {164ms} primitives
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- `933 tallies` is the sample count. Below a few hundred, small differences
|
|
140
|
+
mean nothing.
|
|
141
|
+
- A percentage is of the whole spied block and includes everything below it.
|
|
142
|
+
Own time is a node's percentage minus its children's.
|
|
143
|
+
- `Receiver(DefiningClass)>>selector` names where an inherited method is
|
|
144
|
+
defined: `Array(SequenceableCollection)>>shuffle` runs on an `Array` and is
|
|
145
|
+
defined on `SequenceableCollection`.
|
|
146
|
+
- `primitives` is not a method. It is the part of the parent's time no child
|
|
147
|
+
accounts for: its own work, primitives and inlined code included.
|
|
148
|
+
- The `|` bars are tree drawing. A node gets one when a sibling follows it at
|
|
149
|
+
that level, and carries it down its subtree. They mean nothing else.
|
|
150
|
+
- `**Leaves**` ranks by own time and is usually what to act on. The tree says
|
|
151
|
+
who is responsible, the leaves say what is expensive.
|
|
152
|
+
- `**Memory**` is the allocation delta over the run and `**GCs**` the
|
|
153
|
+
collections and the share of the run spent in them. Heavy incremental GC with
|
|
154
|
+
a large young delta is an allocation problem, and optimizing the leaves will
|
|
155
|
+
not touch it.
|
|
156
|
+
|
|
157
|
+
## Whole-Image Profiling
|
|
158
|
+
|
|
159
|
+
Use it when the work happens in another process, or when the question is about
|
|
160
|
+
the image rather than one block.
|
|
161
|
+
|
|
162
|
+
```smalltalk
|
|
163
|
+
| profiler |
|
|
164
|
+
profiler := AndreasSystemProfiler new.
|
|
165
|
+
profiler spyOn: [ "..." ].
|
|
166
|
+
String streamContents: [ :stream | profiler report: stream cutoff: 2 ]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Never send `spyOn:` to the class: it ends in `doReport`, which opens a window.
|
|
170
|
+
The instance-side `spyOn:` is the headless path.
|
|
171
|
+
|
|
172
|
+
Its tree is rooted at the image, so a report taken through MCP opens with the
|
|
173
|
+
frames of the server handling the call. Find the workload's entry method and
|
|
174
|
+
read the percentages from that node down.
|
|
175
|
+
|
|
176
|
+
## Measuring Instead Of Profiling
|
|
177
|
+
|
|
178
|
+
A profile ranks, it does not measure. For a before-and-after number:
|
|
179
|
+
|
|
180
|
+
```smalltalk
|
|
181
|
+
"one run, as a Duration"
|
|
182
|
+
[ "..." ] timeToRun
|
|
183
|
+
|
|
184
|
+
"iterations per second over five seconds, which absorbs the noise"
|
|
185
|
+
[ "..." ] bench
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Time the same thing at least three times and report the minimum rather than the
|
|
189
|
+
mean: the minimum is the run least disturbed by GC and scheduling.
|
|
190
|
+
|
|
191
|
+
## Stop Before
|
|
192
|
+
|
|
193
|
+
- Profiling forks a process and leaves state behind. Run it inside an approved
|
|
194
|
+
image boundary and never save the image.
|
|
195
|
+
- Only one `MessageTally` can run at a time; a second answers "it seems a tally
|
|
196
|
+
is already running". Let a forked profile finish before starting another.
|
|
197
|
+
- Do not trust a report whose tree does not contain the workload's own methods:
|
|
198
|
+
it was too short, or it waited instead of computing.
|
|
199
|
+
- Do not compare numbers across different workloads or cutoffs. Compare shapes.
|
|
200
|
+
|
|
201
|
+
## Report
|
|
202
|
+
|
|
203
|
+
Report the workload and how long it ran, the sampler and cutoff, the top tree
|
|
204
|
+
nodes and leaves with their percentages, the change made, the before-and-after
|
|
205
|
+
comparison, and the tests that showed behavior unchanged.
|
|
@@ -21,6 +21,9 @@ not prove files changed on disk.
|
|
|
21
21
|
and documentation path.
|
|
22
22
|
5. Commit only the intended diff. Push only when explicitly requested or
|
|
23
23
|
clearly approved.
|
|
24
|
+
6. When the checkout moves under the image — switching branch, pulling, or any
|
|
25
|
+
Git command that rewrites the source files — reload the image from those
|
|
26
|
+
files afterwards, or it keeps running the code the old checkout held.
|
|
24
27
|
|
|
25
28
|
## Stop Before Mutation
|
|
26
29
|
|
|
@@ -11,7 +11,7 @@ guessing from source.
|
|
|
11
11
|
- Prefer the least-mutating dedicated tool. Use `image_evaluate` only when no
|
|
12
12
|
dedicated tool fits.
|
|
13
13
|
- Treat image, Tonel, Iceberg, and Git state as separate.
|
|
14
|
-
-
|
|
14
|
+
- Tools change the running image but never save it; use a safe image boundary.
|
|
15
15
|
|
|
16
16
|
## Context Loading
|
|
17
17
|
|
|
@@ -25,6 +25,8 @@ guessing from source.
|
|
|
25
25
|
`skills/pharo-image-git-handoff/SKILL.md`.
|
|
26
26
|
- Before reproducing smalltalkCI or GitHub failures: use
|
|
27
27
|
`skills/pharo-ci-repro/SKILL.md`.
|
|
28
|
+
- Before profiling or optimizing Pharo code: use
|
|
29
|
+
`skills/pharo-code-profiling/SKILL.md`.
|
|
28
30
|
- Before recovering a stopped standalone image: use
|
|
29
31
|
`skills/pharo-mcp-recovery/SKILL.md`.
|
|
30
32
|
- Before debugger sessions or repairs: read the local
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# Debugger-Driven Development
|
|
2
|
-
|
|
3
|
-
Applies when using MCP-Pharo to repair code from a live debugger state.
|
|
4
|
-
|
|
5
|
-
Debugger-driven development keeps execution paused at the failure, edits the
|
|
6
|
-
relevant method, and continues from the debugger instead of restarting the
|
|
7
|
-
whole scenario.
|
|
8
|
-
|
|
9
|
-
## Goal
|
|
10
|
-
|
|
11
|
-
Repair code from the paused execution context while preserving the debugger as
|
|
12
|
-
the source of truth for receiver, selector, class side, selected frame, and
|
|
13
|
-
repair action.
|
|
14
|
-
|
|
15
|
-
## Main Flow
|
|
16
|
-
|
|
17
|
-
1. Get a state with `debug_state_get`, `debug_capture`, or
|
|
18
|
-
`debug_candidate_attach`.
|
|
19
|
-
2. Check `repairActions` before inventing an edit target.
|
|
20
|
-
3. Call `debug_method_update` with the action's `sessionId`, `stateId`, and `frameRef`.
|
|
21
|
-
4. Review returned `critiques`.
|
|
22
|
-
5. If the edit is blocked by critiques and the critiques are acceptable, retry
|
|
23
|
-
with `ignoreCritiques=true`.
|
|
24
|
-
6. Continue with the returned state when execution pauses again.
|
|
25
|
-
|
|
26
|
-
## Must Do
|
|
27
|
-
|
|
28
|
-
- Use the repair action's frame reference instead of selecting a target class or
|
|
29
|
-
method by hand.
|
|
30
|
-
- Review critiques before allowing the repaired computation to proceed.
|
|
31
|
-
- Treat returned states and references as fresh; discard older state-scoped
|
|
32
|
-
references after each edit or control action.
|
|
33
|
-
- Stop after the bounded action the user requested and report the resulting
|
|
34
|
-
state.
|
|
35
|
-
|
|
36
|
-
## Do Not
|
|
37
|
-
|
|
38
|
-
- Do not guess the receiver class, target behavior, selector, or class side for
|
|
39
|
-
missing-method repair.
|
|
40
|
-
- Do not use general `image_evaluate` code to mutate contexts or drive
|
|
41
|
-
debugger UI.
|
|
42
|
-
- Do not continue issuing frame commands when the repair outcome is running,
|
|
43
|
-
timed out, completed, or blocked by critiques.
|
|
44
|
-
|
|
45
|
-
## Missing Methods
|
|
46
|
-
|
|
47
|
-
For `MessageNotUnderstood`, `debug_method_update` creates the method implied by the
|
|
48
|
-
paused message send. The target behavior, selector, class side, and frame are
|
|
49
|
-
derived from the debug state; do not guess them in the request.
|
|
50
|
-
|
|
51
|
-
You may omit `methodSource` when the default generated source is acceptable.
|
|
52
|
-
Provide `methodSource` when the method should do real work. The compiled method
|
|
53
|
-
is formatted before it is returned or used to continue execution.
|
|
54
|
-
|
|
55
|
-
## Stub Methods
|
|
56
|
-
|
|
57
|
-
For `subclassResponsibility`, `shouldBeImplemented`, and `notYetImplemented`,
|
|
58
|
-
`debug_method_update` recompiles the method associated with the selected repair frame.
|
|
59
|
-
Use the repair action's frame reference instead of selecting a target class by
|
|
60
|
-
hand.
|
|
61
|
-
|
|
62
|
-
## Critiques
|
|
63
|
-
|
|
64
|
-
`debug_method_update` uses the same critique gate expected from method-editing tools. It
|
|
65
|
-
stops before proceeding when critiques are reported unless
|
|
66
|
-
`ignoreCritiques=true`.
|
|
67
|
-
|
|
68
|
-
Use `ignoreCritiques=true` only after reading the critiques and deciding that
|
|
69
|
-
continuing the suspended computation is still the right next step.
|
|
70
|
-
|
|
71
|
-
## Proceeding
|
|
72
|
-
|
|
73
|
-
After a successful accepted edit, `debug_method_update` attempts to proceed the repaired
|
|
74
|
-
computation. Outcomes include completed, paused, running, timed out, or blocked
|
|
75
|
-
by critiques.
|
|
76
|
-
|
|
77
|
-
When the outcome is paused, use the returned state and repair actions. When it
|
|
78
|
-
is completed, inspect the result class and print string. When it is running or
|
|
79
|
-
timed out, avoid issuing stale frame commands.
|
|
80
|
-
|
|
81
|
-
## Human-Owned Debuggers
|
|
82
|
-
|
|
83
|
-
For debugger windows opened by a human, discover candidates with
|
|
84
|
-
`debug_candidate_search`, attach with `debug_candidate_attach`, and let the
|
|
85
|
-
debugger controller perform control actions. The visible debugger remains the
|
|
86
|
-
UI owner. Do not use general `image_evaluate` code to drive Morphic debugger
|
|
87
|
-
widgets or mutate contexts directly.
|
|
88
|
-
|
|
89
|
-
When the user asks you to perform a bounded action and give control back, run
|
|
90
|
-
only the requested debugger calls, then stop and report the resulting state.
|
|
91
|
-
|
|
92
|
-
## Reporting
|
|
93
|
-
|
|
94
|
-
When finishing a debugger-driven repair, report:
|
|
95
|
-
|
|
96
|
-
- repair action used;
|
|
97
|
-
- method created or recompiled, when any;
|
|
98
|
-
- critiques returned and whether they were accepted;
|
|
99
|
-
- proceed outcome;
|
|
100
|
-
- final state or why no fresh state is available;
|
|
101
|
-
- cleanup still needed.
|