@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.
@@ -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.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Pharo Code Profiling"
3
+ short_description: "Find Pharo performance hotspots"
4
+ default_prompt: "Use $pharo-code-profiling to profile this Pharo code and find where the time is spent before optimizing."
@@ -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
- - Successful mutating tools save the image; use a safe image boundary.
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.