14 |
15 |
17 |
18 |
59 |
60 |
61 |
62 |
110 | 6 //
111 | 7 //
112 | .
113 | .
114 | ```
115 |
116 | ## Usage
117 |
118 | ### Activate
119 |
120 | You can now activate your PLC on your target and work as you are used to. Note that the Profiler adds some overhead to your code. making execution a bit slower. Usually you should not notice a big impact though. To start profiling, login to your PLC, navigate to your MAIN programm, right click on *Profiler* (In the line you manually inserted in your code) and then click `Add Watch`.
121 |
122 |
123 |
124 |
125 |
126 |
127 | Then search for *Twingrind.Profiler* in the Watch panel and expand the node. You can then use the watch window to
128 | - **Capture the callstack** of a single frame of your PLC by a rising edge of *CaptureOnce*
129 | - Run **Captures continuously** by setting *CaptureContinuous=TRUE*.
130 | - You can specify a cpu time threshold such that only frames with a certain percentage-based usage of your CPU are captured (`CaptureCpuTimeLowThreshold`, `Capture CpuTimeHighThreshold`).
131 | - You can use `Mode` to adjust which callstacks are stored by the Profiler. For instance setting `Mode=Slowest` will only keep the slowest callstacks in the storage.
132 | - The library includes a parameter *MAX_FRAMES*, which is used to adjust the maximum amount of recorded frames. If *FrameIndex=MAX_FRAMES* no
133 | new captures will be performed by the Profiler. In order to **reset already taken recordings** you can give a rising edge on *Reset*. This will
134 | internally remove all data and set *FrameIndex=0* again.
135 |
136 | ### Process snapshot
137 |
138 | The *process* command reads all callstacks that have been recorded from the PLC and then reconstructs a callgrind file. Usually this is the command that you want to work with.
139 |
140 | ```
141 | twingrind process -m hashmap
142 | ```
143 |
144 | Here `-m hashmap` refers to the hashmap that has been created for your PLC during preparation. Use `twingrind process -h` for a detailed listing of all arguments. Use the following command to delete any previously recorded data (`-r` argument), take the profile of a single cycle (`-s1` argument).
145 | If the command fails with an "RecursionError: maximum recursion depth exceeded" error, try to increase the recursion limit with the "--recursion-limit N" switch, the recursion limit defaults to 2000.
146 |
147 | ```
148 | twingrind process -m hashmap -rs1
149 | ```
150 |
151 | ### Optional: Only read out profiling data from the PLC
152 |
153 | Run the following command to only read out all data from your PLC.
154 |
155 | ```
156 | twingrind fetch
157 | ```
158 |
159 | to read all recorded frames from the PLC. Capturing of callstacks is temporarily disabled. The resulting data is the output of *fetch* and is stored in
160 | the current directory. Latter files contain base64 encoded information about the callstack and can be
161 | converted to the callgrind format by *reconstruct*. The *fetch* command per default connects to the local target
162 | and with the PLC that is running on port 851. However, the command has several arguments to control its behavior, use
163 | `twingrind fetch -h` for a detailed listing.
164 |
165 |
166 | ### Optional: Convert previously read out data to callgrind
167 |
168 | Use the following command to reconstruct a frame.
169 |
170 | ```
171 | twingrind reconstruct -m -c
172 | ```
173 |
174 | Creates a callgrind file in the current directory. This script uses a previously generated hashmap (output of *prepare*) together with a recorded callstack (output of *fetch*).
175 | Run the reconstruct command for all frames that were exported by *fetch*.
176 | You may then open [qcachegrind](http://kcachegrind.sourceforge.net/html/Home.html) to visualize the callstack of your
177 | captured cycles. The command comes with some arguments to control its behavior, for details refer to `twingrind reconstruct -h`
178 |
179 | In the images below the first one shows the overview over a complete cycle. The PLC that I was running when taking this picture didn't use a lot of cpu ticks that is why there is a lot of empty space in *CYCLE::CYCLE*. The second image is zoomed into the MAIN PRG.
180 | If the command fails with an "RecursionError: maximum recursion depth exceeded" error, try to increase the recursion limit with the "--recursion-limit N" switch, the recursion limit defaults to 2000.
181 |
182 |
183 |
184 |
185 |
186 |
187 | ## Cleanup
188 |
189 | To cleanup your code from code that was added in the *Prepare* section you can run the *clean* as follows
190 |
191 | ```
192 | twingrind clean -d
193 | ```
194 |
195 | The command transverses through the entire code base of the plc located at the given directory.
196 | For all methods, the command removes the header function call and a the footer function call to the profiler
197 | library that were previously generated by using the "prepare".
198 |
--------------------------------------------------------------------------------
/Twingrind/Twingrind.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # TcXaeShell Solution File, Format Version 11.00
4 | VisualStudioVersion = 15.0.28307.1300
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{DFBE7525-6864-4E62-8B2E-D530D69D9D96}") = "Twingrind", "Twingrind\Twingrind.tspproj", "{A16004EA-33C0-4358-8B6F-71035443BB89}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|TwinCAT CE7 (ARMV7) = Debug|TwinCAT CE7 (ARMV7)
11 | Debug|TwinCAT OS (ARMT2) = Debug|TwinCAT OS (ARMT2)
12 | Debug|TwinCAT RT (x64) = Debug|TwinCAT RT (x64)
13 | Debug|TwinCAT RT (x86) = Debug|TwinCAT RT (x86)
14 | Release|TwinCAT CE7 (ARMV7) = Release|TwinCAT CE7 (ARMV7)
15 | Release|TwinCAT OS (ARMT2) = Release|TwinCAT OS (ARMT2)
16 | Release|TwinCAT RT (x64) = Release|TwinCAT RT (x64)
17 | Release|TwinCAT RT (x86) = Release|TwinCAT RT (x86)
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Debug|TwinCAT CE7 (ARMV7).ActiveCfg = Debug|TwinCAT CE7 (ARMV7)
21 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Debug|TwinCAT CE7 (ARMV7).Build.0 = Debug|TwinCAT CE7 (ARMV7)
22 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Debug|TwinCAT OS (ARMT2).ActiveCfg = Debug|TwinCAT OS (ARMT2)
23 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Debug|TwinCAT OS (ARMT2).Build.0 = Debug|TwinCAT OS (ARMT2)
24 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Debug|TwinCAT RT (x64).ActiveCfg = Debug|TwinCAT RT (x64)
25 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Debug|TwinCAT RT (x64).Build.0 = Debug|TwinCAT RT (x64)
26 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Debug|TwinCAT RT (x86).ActiveCfg = Debug|TwinCAT RT (x86)
27 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Debug|TwinCAT RT (x86).Build.0 = Debug|TwinCAT RT (x86)
28 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Release|TwinCAT CE7 (ARMV7).ActiveCfg = Release|TwinCAT CE7 (ARMV7)
29 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Release|TwinCAT CE7 (ARMV7).Build.0 = Release|TwinCAT CE7 (ARMV7)
30 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Release|TwinCAT OS (ARMT2).ActiveCfg = Release|TwinCAT OS (ARMT2)
31 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Release|TwinCAT OS (ARMT2).Build.0 = Release|TwinCAT OS (ARMT2)
32 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Release|TwinCAT RT (x64).ActiveCfg = Release|TwinCAT RT (x64)
33 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Release|TwinCAT RT (x64).Build.0 = Release|TwinCAT RT (x64)
34 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Release|TwinCAT RT (x86).ActiveCfg = Release|TwinCAT RT (x86)
35 | {A16004EA-33C0-4358-8B6F-71035443BB89}.Release|TwinCAT RT (x86).Build.0 = Release|TwinCAT RT (x86)
36 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Debug|TwinCAT CE7 (ARMV7).ActiveCfg = Debug|TwinCAT CE7 (ARMV7)
37 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Debug|TwinCAT CE7 (ARMV7).Build.0 = Debug|TwinCAT CE7 (ARMV7)
38 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Debug|TwinCAT OS (ARMT2).ActiveCfg = Debug|TwinCAT OS (ARMT2)
39 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Debug|TwinCAT OS (ARMT2).Build.0 = Debug|TwinCAT OS (ARMT2)
40 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Debug|TwinCAT RT (x64).ActiveCfg = Debug|TwinCAT RT (x64)
41 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Debug|TwinCAT RT (x64).Build.0 = Debug|TwinCAT RT (x64)
42 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Debug|TwinCAT RT (x86).ActiveCfg = Debug|TwinCAT RT (x86)
43 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Debug|TwinCAT RT (x86).Build.0 = Debug|TwinCAT RT (x86)
44 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Release|TwinCAT CE7 (ARMV7).ActiveCfg = Release|TwinCAT CE7 (ARMV7)
45 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Release|TwinCAT CE7 (ARMV7).Build.0 = Release|TwinCAT CE7 (ARMV7)
46 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Release|TwinCAT OS (ARMT2).ActiveCfg = Release|TwinCAT OS (ARMT2)
47 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Release|TwinCAT OS (ARMT2).Build.0 = Release|TwinCAT OS (ARMT2)
48 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Release|TwinCAT RT (x64).ActiveCfg = Release|TwinCAT RT (x64)
49 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Release|TwinCAT RT (x64).Build.0 = Release|TwinCAT RT (x64)
50 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Release|TwinCAT RT (x86).ActiveCfg = Release|TwinCAT RT (x86)
51 | {C3E0E12E-171D-4C3C-8375-B12DC038DFE6}.Release|TwinCAT RT (x86).Build.0 = Release|TwinCAT RT (x86)
52 | EndGlobalSection
53 | GlobalSection(SolutionProperties) = preSolution
54 | HideSolutionNode = FALSE
55 | EndGlobalSection
56 | GlobalSection(ExtensibilityGlobals) = postSolution
57 | SolutionGuid = {0DEEEE26-4F96-4C30-B199-81292F2AFAF4}
58 | EndGlobalSection
59 | EndGlobal
60 |
--------------------------------------------------------------------------------
/Twingrind/Twingrind.tspproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Twingrind/Twingrind/Twingrind.tspproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Twingrind/Twingrind/Twingrind/CaptureMode.TcDUT:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
--------------------------------------------------------------------------------
/Twingrind/Twingrind/Twingrind/FrameData.TcDUT:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
--------------------------------------------------------------------------------
/Twingrind/Twingrind/Twingrind/FrameMeta.TcDUT:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
--------------------------------------------------------------------------------
/Twingrind/Twingrind/Twingrind/ParameterList.TcGVL:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/Twingrind/Twingrind/Twingrind/Profiler.TcPOU:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
46 |
47 | SINT_TO_UDINT(MAX_TASKS)
59 | THEN
60 | Error := TRUE;
61 | ErrorMessage := 'Twingrind only supports PLCs with 1 tasks only!';
62 | RETURN;
63 | END_IF
64 |
65 | Tasks := UDINT_TO_SINT(TwinCAT_SystemInfoVarList._AppInfo.TaskCnt);
66 | FOR _taskIt:=1 TO TwinCAT_SystemInfoVarList._AppInfo.TaskCnt
67 | DO
68 | CycleTime[_taskIt] := TwinCAT_SystemInfoVarList._TaskInfo[_taskIt].CycleTime;
69 | END_FOR
70 | END_IF
71 |
72 | IF _captureContinuousTrig.Q
73 | THEN
74 | CaptureOnce := 0;
75 | Error := FALSE;
76 | ErrorMessage := '';
77 | END_IF
78 |
79 | // Logic to only keep frames that satisfy the threshold conditions
80 | IF _frameRecorded.Q
81 | THEN
82 | _meta.Size := _size;
83 | _meta.TotalDuration := _data[_size-1].EndHi - _data[0].StartHi + _data[_size-1].EndLo - _data[0].StartLo;
84 | _currenttask();
85 |
86 | // Check if the frame that was just recorded should be kept, if so, find the next place in the date structure where
87 | // we can write into
88 | _cycleDuration := 100 * UDINT_TO_LREAL(TwinCAT_SystemInfoVarList._TaskInfo[_currenttask.index].LastExecTime) / UDINT_TO_LREAL(TwinCAT_SystemInfoVarList._TaskInfo[_currenttask.index].CycleTime);
89 | IF (_cycleDuration > CaptureCpuTimeLowThreshold AND_THEN _cycleDuration < CaptureCpuTimeHighThreshold) OR_ELSE
90 | (CaptureCpuTimeLowThreshold = 0 AND_THEN CaptureCpuTimeHighThreshold = 0)
91 | THEN
92 | FrameIndex := NextFrameIndex();
93 | END_IF
94 |
95 | END_IF
96 |
97 | IF CaptureContinuous
98 | THEN
99 | _enabled := TRUE;
100 | _size := 0;
101 | ELSE
102 | IF _captureOnceTrig.Q
103 | THEN
104 | Error := FALSE;
105 | ErrorMessage := '';
106 | _enabled := TRUE;
107 | _size := 0;
108 | END_IF
109 | END_IF
110 |
111 | // Delete all Ddata that has already been caputured
112 | IF _resetTrig.Q
113 | THEN
114 | Error := FALSE;
115 | ErrorMessage := '';
116 | FrameIndex := 0;
117 | MEMSET(ADR(Data), 0, SIZEOF(Data));
118 | MEMSET(ADR(Meta), 0, SIZEOF(Meta));
119 | _enabled := FALSE;
120 | END_IF
121 |
122 | // Prepare the current frame
123 | Busy := _enabled;
124 | _depth := 0;
125 | _data REF= Data[FrameIndex, 1];
126 | _meta REF= Meta[FrameIndex];
127 | _meta.Id := TwinCAT_SystemInfoVarList._TaskInfo[1].CycleCount;
128 | _frameRecorded(CLK:=_enabled);]]>
129 |
130 |
131 |
137 |
138 | ParameterList.MAX_FRAMES
160 | THEN
161 | NextFrameIndex := 0;
162 | END_IF
163 |
164 | // Find the index of the slowest profile
165 | CaptureMode.Fastest:
166 | duration := 0;
167 | FOR i:=0 TO ParameterList.MAX_FRAMES
168 | DO
169 | IF Meta[i].TotalDuration = 0
170 | THEN
171 | NextFrameIndex := i;
172 | RETURN;
173 | ELSIF Meta[i].TotalDuration > duration OR_ELSE duration = 0
174 | THEN
175 | duration := Meta[i].TotalDuration;
176 | NextFrameIndex := i;
177 | END_IF
178 | END_FOR
179 |
180 | // Only record max_frames, then no further frames are caputured
181 | CaptureMode.FirstOnesOnly:
182 | NextFrameIndex := MIN(FrameIndex + 1, ParameterList.MAX_FRAMES);
183 |
184 | END_CASE]]>
185 |
186 |
187 |
188 |
192 |
193 | _data[_size].EndLo, cpuCntHiDW => _data[_size].EndHi);
204 |
205 | IF _depth < 0
206 | THEN
207 | _enabled := FALSE;
208 | Error := TRUE;
209 | ErrorMessage := 'Pop/Push mismatch!';
210 | RETURN;
211 | ELSE
212 | _size := _size + 1;
213 | END_IF
214 |
215 | // abort if the stack is getting too big (too many functions were called)
216 | IF _size > ParameterList.MAX_STACKSIZE
217 | THEN
218 | _enabled := FALSE;
219 | Error := TRUE;
220 | ErrorMessage := 'The method stack is too big!';
221 | END_IF]]>
222 |
223 |
224 |
225 |
231 |
232 | _data[_size].StartLo, cpuCntHiDW => _data[_size].StartHi);
240 | _data[_size].endhi := 0;
241 | _data[_size].endlo := 0;
242 | _size := _size + 1;
243 | _depth := _depth + 1;
244 |
245 | // abort if the stack is getting too big (too many functions were called)
246 | IF _size > ParameterList.MAX_STACKSIZE
247 | THEN
248 | _enabled := FALSE;
249 | Error := TRUE;
250 | ErrorMessage := 'The method stack is too big!';
251 | END_IF
252 | ]]>
253 |
254 |
255 |
256 |
--------------------------------------------------------------------------------
/Twingrind/Twingrind/Twingrind/ProfilerStackStruct.TcDUT:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
--------------------------------------------------------------------------------
/Twingrind/Twingrind/Twingrind/Twingrind.plcproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 1.0.0.0
6 | 2.0
7 | {c3e0e12e-171d-4c3c-8375-b12dc038dfe6}
8 | True
9 | Twingrind
10 | 3.1.4023.0
11 | {5cb8600d-36c2-4aed-9484-2799a420f128}
12 | {ee1e3ca8-77f5-4cde-9c10-f6855ded4270}
13 | {1c562155-e637-4e3e-83a9-6925d9835630}
14 | {03206031-9847-428b-a9f4-bc7adfe37b16}
15 | {a4e318aa-bf97-40bc-88f0-4821b943304c}
16 | {6cc762c1-16b6-4d36-aa28-1ac36cb62ce1}
17 | false
18 | Stefan Besler
19 | false
20 | Twingrind
21 | 0.4.1.0
22 | Stefan Besler and Contributers
23 |
26 |
27 |
28 |
29 | Code
30 |
31 |
32 | Code
33 |
34 |
35 | Code
36 | true
37 |
38 |
39 | Code
40 |
41 |
42 | Code
43 |
44 |
45 |
46 |
47 | Tc2_Standard, * (Beckhoff Automation GmbH)
48 | Tc2_Standard
49 |
50 |
51 | Tc2_System, * (Beckhoff Automation GmbH)
52 | Tc2_System
53 |
54 |
55 |
56 |
57 | Tc2_Standard, * (Beckhoff Automation GmbH)
58 |
59 |
60 | Tc2_System, * (Beckhoff Automation GmbH)
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | "<ProjectRoot>"
69 |
70 | {192FAD59-8248-4824-A8DE-9177C94C195A}
71 |
72 | "{192FAD59-8248-4824-A8DE-9177C94C195A}"
73 |
74 |
75 |
76 | {246001F4-279D-43AC-B241-948EB31120E1}
77 |
78 | "{246001F4-279D-43AC-B241-948EB31120E1}"
79 |
80 |
81 | GlobalVisuImageFilePath
82 | %APPLICATIONPATH%
83 |
84 |
85 | {F66C7017-BDD8-4114-926C-81D6D687E35F}
86 |
87 | "{F66C7017-BDD8-4114-926C-81D6D687E35F}"
88 |
89 |
90 |
91 | {40450F57-0AA3-4216-96F3-5444ECB29763}
92 |
93 | "{40450F57-0AA3-4216-96F3-5444ECB29763}"
94 |
95 |
96 | ActiveVisuProfile
97 | IR0whWr8bwfwBwAAiD2qpQAAAABVAgAA37x72QAAAAABAAAAAAAAAAEaUwB5AHMAdABlAG0ALgBTAHQAcgBpAG4AZwACTHsAZgA5ADUAYgBiADQAMgA2AC0ANQA1ADIANAAtADQAYgA0ADUALQA5ADQAMAAwAC0AZgBiADAAZgAyAGUANwA3AGUANQAxAGIAfQADCE4AYQBtAGUABDBUAHcAaQBuAEMAQQBUACAAMwAuADEAIABCAHUAaQBsAGQAIAA0ADAAMgA0AC4ANwAFFlAAcgBvAGYAaQBsAGUARABhAHQAYQAGTHsAMQA2AGUANQA1AGIANgAwAC0ANwAwADQAMwAtADQAYQA2ADMALQBiADYANQBiAC0ANgAxADQANwAxADMAOAA3ADgAZAA0ADIAfQAHEkwAaQBiAHIAYQByAGkAZQBzAAhMewAzAGIAZgBkADUANAA1ADkALQBiADAANwBmAC0ANABkADYAZQAtAGEAZQAxAGEALQBhADgAMwAzADUANgBhADUANQAxADQAMgB9AAlMewA5AGMAOQA1ADgAOQA2ADgALQAyAGMAOAA1AC0ANAAxAGIAYgAtADgAOAA3ADEALQA4ADkANQBmAGYAMQBmAGUAZABlADEAYQB9AAoOVgBlAHIAcwBpAG8AbgALBmkAbgB0AAwKVQBzAGEAZwBlAA0KVABpAHQAbABlAA4aVgBpAHMAdQBFAGwAZQBtAE0AZQB0AGUAcgAPDkMAbwBtAHAAYQBuAHkAEAxTAHkAcwB0AGUAbQARElYAaQBzAHUARQBsAGUAbQBzABIwVgBpAHMAdQBFAGwAZQBtAHMAUwBwAGUAYwBpAGEAbABDAG8AbgB0AHIAbwBsAHMAEyhWAGkAcwB1AEUAbABlAG0AcwBXAGkAbgBDAG8AbgB0AHIAbwBsAHMAFCRWAGkAcwB1AEUAbABlAG0AVABlAHgAdABFAGQAaQB0AG8AcgAVIlYAaQBzAHUATgBhAHQAaQB2AGUAQwBvAG4AdAByAG8AbAAWFHYAaQBzAHUAaQBuAHAAdQB0AHMAFwxzAHkAcwB0AGUAbQAYGFYAaQBzAHUARQBsAGUAbQBCAGEAcwBlABkmRABlAHYAUABsAGEAYwBlAGgAbwBsAGQAZQByAHMAVQBzAGUAZAAaCGIAbwBvAGwAGyJQAGwAdQBnAGkAbgBDAG8AbgBzAHQAcgBhAGkAbgB0AHMAHEx7ADQAMwBkADUAMgBiAGMAZQAtADkANAAyAGMALQA0ADQAZAA3AC0AOQBlADkANAAtADEAYgBmAGQAZgAzADEAMABlADYAMwBjAH0AHRxBAHQATABlAGEAcwB0AFYAZQByAHMAaQBvAG4AHhRQAGwAdQBnAGkAbgBHAHUAaQBkAB8WUwB5AHMAdABlAG0ALgBHAHUAaQBkACBIYQBmAGMAZAA1ADQANAA2AC0ANAA5ADEANAAtADQAZgBlADcALQBiAGIANwA4AC0AOQBiAGYAZgBlAGIANwAwAGYAZAAxADcAIRRVAHAAZABhAHQAZQBJAG4AZgBvACJMewBiADAAMwAzADYANgBhADgALQBiADUAYwAwAC0ANABiADkAYQAtAGEAMAAwAGUALQBlAGIAOAA2ADAAMQAxADEAMAA0AGMAMwB9ACMOVQBwAGQAYQB0AGUAcwAkTHsAMQA4ADYAOABmAGYAYwA5AC0AZQA0AGYAYwAtADQANQAzADIALQBhAGMAMAA2AC0AMQBlADMAOQBiAGIANQA1ADcAYgA2ADkAfQAlTHsAYQA1AGIAZAA0ADgAYwAzAC0AMABkADEANwAtADQAMQBiADUALQBiADEANgA0AC0ANQBmAGMANgBhAGQAMgBiADkANgBiADcAfQAmFk8AYgBqAGUAYwB0AHMAVAB5AHAAZQAnVFUAcABkAGEAdABlAEwAYQBuAGcAdQBhAGcAZQBNAG8AZABlAGwARgBvAHIAQwBvAG4AdgBlAHIAdABpAGIAbABlAEwAaQBiAHIAYQByAGkAZQBzACgQTABpAGIAVABpAHQAbABlACkUTABpAGIAQwBvAG0AcABhAG4AeQAqHlUAcABkAGEAdABlAFAAcgBvAHYAaQBkAGUAcgBzACs4UwB5AHMAdABlAG0ALgBDAG8AbABsAGUAYwB0AGkAbwBuAHMALgBIAGEAcwBoAHQAYQBiAGwAZQAsEnYAaQBzAHUAZQBsAGUAbQBzAC1INgBjAGIAMQBjAGQAZQAxAC0AZAA1AGQAYwAtADQAYQAzAGIALQA5ADAANQA0AC0AMgAxAGYAYQA3ADUANgBhADMAZgBhADQALihJAG4AdABlAHIAZgBhAGMAZQBWAGUAcgBzAGkAbwBuAEkAbgBmAG8AL0x7AGMANgAxADEAZQA0ADAAMAAtADcAZgBiADkALQA0AGMAMwA1AC0AYgA5AGEAYwAtADQAZQAzADEANABiADUAOQA5ADYANAAzAH0AMBhNAGEAagBvAHIAVgBlAHIAcwBpAG8AbgAxGE0AaQBuAG8AcgBWAGUAcgBzAGkAbwBuADIMTABlAGcAYQBjAHkAMzBMAGEAbgBnAHUAYQBnAGUATQBvAGQAZQBsAFYAZQByAHMAaQBvAG4ASQBuAGYAbwA0MEwAbwBhAGQATABpAGIAcgBhAHIAaQBlAHMASQBuAHQAbwBQAHIAbwBqAGUAYwB0ADUaQwBvAG0AcABhAHQAaQBiAGkAbABpAHQAeQDQAAIaA9ADAS0E0AUGGgfQBwgaAUUHCQjQAAkaBEUKCwQDAAAABQAAAA0AAAAAAAAA0AwLrQIAAADQDQEtDtAPAS0Q0AAJGgRFCgsEAwAAAAUAAAANAAAAKAAAANAMC60BAAAA0A0BLRHQDwEtENAACRoERQoLBAMAAAAFAAAADQAAAAAAAADQDAutAgAAANANAS0S0A8BLRDQAAkaBEUKCwQDAAAABQAAAA0AAAAUAAAA0AwLrQIAAADQDQEtE9APAS0Q0AAJGgRFCgsEAwAAAAUAAAANAAAAAAAAANAMC60CAAAA0A0BLRTQDwEtENAACRoERQoLBAMAAAAFAAAADQAAAAAAAADQDAutAgAAANANAS0V0A8BLRDQAAkaBEUKCwQDAAAABQAAAA0AAAAAAAAA0AwLrQIAAADQDQEtFtAPAS0X0AAJGgRFCgsEAwAAAAUAAAANAAAAKAAAANAMC60EAAAA0A0BLRjQDwEtENAZGq0BRRscAdAAHBoCRR0LBAMAAAAFAAAADQAAAAAAAADQHh8tINAhIhoCRSMkAtAAJRoFRQoLBAMAAAADAAAAAAAAAAoAAADQJgutAAAAANADAS0n0CgBLRHQKQEtENAAJRoFRQoLBAMAAAADAAAAAAAAAAoAAADQJgutAQAAANADAS0n0CgBLRHQKQEtEJoqKwFFAAEC0AABLSzQAAEtF9AAHy0t0C4vGgPQMAutAQAAANAxC60XAAAA0DIarQDQMy8aA9AwC60CAAAA0DELrQMAAADQMhqtANA0Gq0A0DUarQA=
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | System.Collections.Hashtable
106 | {54dd0eac-a6d8-46f2-8c27-2f43c7e49861}
107 | System.String
108 |
109 |
110 |
111 |
112 |
124 |
125 |
--------------------------------------------------------------------------------
/images/add_library.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/add_library.png
--------------------------------------------------------------------------------
/images/add_watch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/add_watch.png
--------------------------------------------------------------------------------
/images/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/demo.png
--------------------------------------------------------------------------------
/images/demo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/demo1.png
--------------------------------------------------------------------------------
/images/demo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/demo2.png
--------------------------------------------------------------------------------
/images/demo3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/demo3.png
--------------------------------------------------------------------------------
/images/demo4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/demo4.png
--------------------------------------------------------------------------------
/images/demo_struckig.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/demo_struckig.png
--------------------------------------------------------------------------------
/images/demo_struckig1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/demo_struckig1.png
--------------------------------------------------------------------------------
/images/installation_libraryrepository.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/installation_libraryrepository.png
--------------------------------------------------------------------------------
/images/installation_twincatxae.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/installation_twincatxae.png
--------------------------------------------------------------------------------
/images/watch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/images/watch.png
--------------------------------------------------------------------------------
/pytwingrind/pytwingrind/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbesler/twingrind/88940b5da9cfb791e779dfedc5e942a5a8f42510/pytwingrind/pytwingrind/__init__.py
--------------------------------------------------------------------------------
/pytwingrind/pytwingrind/__main__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import logging
3 | from argparse import ArgumentParser
4 |
5 | prepare_parser = ArgumentParser("""Prepares the source code of a PLC
6 | so that is can be used with Twingrind. The script parses through all source files (*.POU)
7 | and adds boilerplate code to every functionblock, function and method in the PLC.
8 |
9 | The output of this command is a hashmap, which is a mapping between a uniquely generated id and
10 | the functionblock-, function- or methodname, respectively. The hashmap file has to be provided when
11 | reconstructiong the call-graph.
12 | """)
13 | prepare_parser.add_argument("-d", "--directory", help="Directory containing the PLC project and all source files", required=True)
14 | prepare_parser.add_argument("-m", "--hashmap", help="Filepath of a hashmap, if the file does not exist, it will be created", required=True)
15 |
16 | fetch_parser = ArgumentParser("""Reads out all call-graph caputues from a PLC""")
17 | fetch_parser.add_argument("-n", "--netid", help="AMS-NetId of the target machine, defaults to the local machine", default="", required=False)
18 | fetch_parser.add_argument("-p", "--port", help="Port of the PLC", default=851, required=False)
19 | fetch_parser.add_argument("-d", "--directory", help="Output directory", default="./", required=False)
20 | fetch_parser.add_argument("-o", "--outputname", help="Outputname prefix for files that are generated", default="callstack", required=False)
21 | fetch_parser.add_argument("-N", "--namespace", help="Namespace that is used for the Twingrind library, useful if used with TC_SYM_WITH_NAMESPACE", default="Twingrind", required=False)
22 | fetch_parser.add_argument("-r", "--reset", help="Reset the profiler, this action is taken before taken new shots using the shots argument", action='store_true')
23 | fetch_parser.add_argument("-s", "--shots", help="How many single shots should be taken when calling the fetch command", default=0, required=False, type=int)
24 |
25 | reconstruct_parser = ArgumentParser("""Converts a callstack, as it has been read of the fetch command together with the
26 | hashmap that has been created for the PLC with the prepare command, to the callgrind format.""")
27 | reconstruct_parser.add_argument("-m", "--hashmap", help="Hashmap that is created with the prepare command", required=False)
28 | reconstruct_parser.add_argument("-c", "--callstack", help="Callstack that was read out with the fetch command", required=True)
29 | reconstruct_parser.add_argument("-d", "--directory", help="Output directory", default="./", required=False)
30 | reconstruct_parser.add_argument("-q", "--masquarade", help="Obfuscate names of functionblocks, functions and methods", action="store_true", required=False)
31 | reconstruct_parser.add_argument("-o", "--outputname", help="Outputname prefix for files that are generated", default="callstack", required=False)
32 | reconstruct_parser.add_argument("-R", "--recursion-limit", help="Set pythons maximum recursion limit", default=2000, required=False)
33 |
34 | process_parser = ArgumentParser("""Fetches all captures from the PLC and then reconstructs the call-graph. This command
35 | is the same as running fetch and then reconstructing every callstack""")
36 | process_parser.add_argument("-n", "--netid", help="AMS-NetId of the target machine, defaults to the local machine", default="", required=False)
37 | process_parser.add_argument("-p", "--port", help="Port of the PLC", default=851, required=False)
38 | process_parser.add_argument("-d", "--directory", help="Output directory", default="./", required=False)
39 | process_parser.add_argument("-m", "--hashmap", help="Hashmap that is created with the prepare command", required=True)
40 | process_parser.add_argument("-q", "--masquarade", help="Obfuscate names of functionblocks, functions and methods", action="store_true", required=False)
41 | process_parser.add_argument("-o", "--outputname", help="Outputname prefix for files that are generated", default="callstack", required=False)
42 | process_parser.add_argument("-N", "--namespace", help="Namespace that is used for the Twingrind library, useful if used with TC_SYM_WITH_NAMESPACE", default="Twingrind", required=False)
43 | process_parser.add_argument("-r", "--reset", help="Reset the profiler, this action is taken before taken new shots using the shots argument", action='store_true')
44 | process_parser.add_argument("-s", "--shots", help="How many single shots should be taken when calling the fetch command", default=0, required=False, type=int)
45 | process_parser.add_argument("-R", "--recursion-limit", help="Set pythons maximum recursion limit", default=2000, required=False)
46 |
47 | clean_parser = ArgumentParser("""Removes all boilerplate code that has been added the PLC with the prepare command.
48 | Use this command if profiling is no longer needed.
49 | """)
50 | clean_parser.add_argument("-d", "--directory", help="Directory containing the PLC project and all source files", required=True)
51 |
52 | def main():
53 | logging.basicConfig(level=logging.DEBUG)
54 | cmds = ["prepare", "fetch", "reconstruct", "process", "clean"]
55 | if len(sys.argv) <= 1 or sys.argv[1] not in cmds:
56 | logging.error(f"""Invalid command. The first arugment must be one of the following items {cmds}""")
57 | sys.exit(-1)
58 |
59 | arg = sys.argv[1]
60 | if arg == "prepare":
61 | import pytwingrind.prepare
62 |
63 | parser = prepare_parser
64 | args = vars(parser.parse_args(sys.argv[2::]))
65 | pytwingrind.prepare.run(args["directory"], args["hashmap"])
66 |
67 | elif arg == "fetch":
68 | import pytwingrind.fetch
69 |
70 | parser = fetch_parser
71 | args = vars(parser.parse_args(sys.argv[2::]))
72 | pytwingrind.fetch.run(args["netid"], int(args["port"]), args["directory"], args["outputname"], args["namespace"], args["reset"], args["shots"])
73 |
74 | elif arg == "reconstruct":
75 | import pytwingrind.reconstruct
76 |
77 | parser = reconstruct_parser
78 | args = vars(parser.parse_args(sys.argv[2::]))
79 | sys.setrecursionlimit(int(args["recursion_limit"]))
80 | pytwingrind.reconstruct.run(args["hashmap"], args["callstack"], args["directory"], args["outputname"])
81 |
82 | elif arg == "process":
83 | import pytwingrind.fetch
84 | import pytwingrind.reconstruct
85 |
86 | parser = process_parser
87 | args = vars(parser.parse_args(sys.argv[2::]))
88 | sys.setrecursionlimit(int(args["recursion_limit"]))
89 | callstacks = pytwingrind.fetch.run(args["netid"], int(args["port"]), args["directory"], args["outputname"], args["namespace"], args["reset"], args["shots"])
90 |
91 | for callstack in callstacks:
92 | pytwingrind.reconstruct.run(args["hashmap"], callstack, args["directory"], "")
93 |
94 | elif arg == "clean":
95 | import pytwingrind.clean
96 |
97 | parser = clean_parser
98 | args = vars(parser.parse_args(sys.argv[2::]))
99 | pytwingrind.clean.run(args["directory"])
100 |
--------------------------------------------------------------------------------
/pytwingrind/pytwingrind/clean.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import os
5 | import re
6 | import logging
7 | from pytwingrind import common
8 |
9 | def remove_guards(filepath: str, fb_name: str):
10 | """remove guards to fb and all methods for this file"""
11 |
12 | encoding = common.detect_encoding(filepath)
13 | with open(filepath, "rt", encoding=encoding) as f:
14 | src = f.read()
15 | src, i = re.subn(r'{tag}Twingrind\.Profiler\.Push.*?{tag}\r?\n'.format(tag=re.escape(common.profiler_tag)), '', src, 0, re.M | re.UNICODE)
16 | src, j = re.subn(r'{tag}Twingrind\.Profiler\.Pop.*?{tag}RETURN'.format(tag=re.escape(common.profiler_tag)), 'RETURN', src, 0, re.M | re.UNICODE)
17 | src, k = re.subn(r'\r?\n{tag}Twingrind\.Profiler\.Pop.*?{tag}'.format(tag=re.escape(common.profiler_tag)), '', src, 0, re.M | re.UNICODE)
18 | logging.debug("{}: removed {} guards".format(fb_name, int(i))) # we should have 2 guards per method
19 |
20 |
21 | with open(filepath, "wt", encoding=encoding) as g:
22 | g.write(src)
23 |
24 | def run(filepath: str):
25 |
26 | for f in common.find_sourcefiles(filepath):
27 | fb_name, _ = os.path.splitext(os.path.basename(f))
28 | remove_guards(f, fb_name)
29 |
--------------------------------------------------------------------------------
/pytwingrind/pytwingrind/common.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | import chardet
3 | import os
4 | import re
5 | from dataclasses import dataclass
6 |
7 | profiler_tag = r"(* @@ PROFILER @@ *)"
8 |
9 | @dataclass
10 | class GuardExclusionToken:
11 | token: str
12 | description: str
13 |
14 | file_skip_tokens = [
15 | GuardExclusionToken(profiler_tag, "Guards already present"),
16 | GuardExclusionToken('', "Sequential function chart detected (SFC)"),
17 | GuardExclusionToken('', "Ladder Logic Diagram detected (LD)"),
18 | GuardExclusionToken('', "Sequential function chart detected (SC)"),
20 | ]
21 |
22 | def detect_encoding(filepath : str):
23 |
24 | with open(filepath, 'rb') as f:
25 | result = chardet.detect(f.read())
26 | return result['encoding']
27 |
28 | return 'utf-8'
29 |
30 | def find_sourcefiles(filepath : str):
31 | """walk recursively through folders and look for TwinCat3 source files"""
32 |
33 | for subdir, _, files in os.walk(filepath):
34 | for f in files:
35 | re_source = re.match(".*.tcpou$", f, re.I)
36 | if re_source:
37 | yield os.path.join(subdir, f)
38 |
39 | class Call(ctypes.Structure):
40 | _fields_ = [("hash", ctypes.c_uint32),
41 | ("depth", ctypes.c_int32),
42 | ("startlo", ctypes.c_uint32),
43 | ("starthi", ctypes.c_uint32),
44 | ("endlo", ctypes.c_uint32),
45 | ("endhi", ctypes.c_uint32)]
46 |
47 | def create_stack_class(size : int):
48 | # create global class to keep pickle happy
49 | global Stack
50 | class Stack(ctypes.Structure):
51 | _fields_ = [("calls", Call * (size))]
52 |
53 | class Callstack(object):
54 | def __init__(self, cycletime : int, task : int, size : int, stack : ctypes.Structure):
55 | self.cycletime = cycletime
56 | self.task = task
57 | self.size = size
58 | self.stack = stack
59 |
--------------------------------------------------------------------------------
/pytwingrind/pytwingrind/fetch.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | import logging
4 | import pyads
5 | import pickle
6 | import ctypes
7 | from pytwingrind import common
8 |
9 | def trigger_edge(plc, symbol: str, pause_duration: float):
10 | plc.write_by_name(symbol, True, pyads.PLCTYPE_BOOL)
11 | time.sleep(pause_duration)
12 | plc.write_by_name(symbol, False, pyads.PLCTYPE_BOOL)
13 | time.sleep(pause_duration)
14 |
15 | def run(netid: str, port: int, directory: str, outputname: str, namespace: str, reset: bool, shots: int):
16 | profiler_symbolname = "Profiler"
17 | parameterlist_symbolname = "ParameterList"
18 |
19 | callstacks = []
20 | is_capturing = False
21 | logging.info(f"Connecting {netid}:{port}")
22 | if netid == "":
23 | pyads.ads.open_port()
24 | netid = pyads.ads.get_local_address().netid
25 | pyads.ads.close_port()
26 |
27 | plc = pyads.Connection(netid, port)
28 | try:
29 | plc.open()
30 |
31 | for i in range(0, 2):
32 | logging.debug(f"Trying to connect to profiler at {profiler_symbolname}")
33 | try:
34 | plc.read_by_name(f"{profiler_symbolname}.CaptureContinuous", pyads.PLCTYPE_BOOL)
35 | except Exception as e:
36 | if i == 0:
37 | profiler_symbolname = ".".join(filter(None, [namespace, "Profiler"]))
38 | parameterlist_symbolname = ".".join(filter(None, [namespace, "ParameterList"]))
39 | else:
40 | raise Exception(f"Could not resolve entry point for profiler, make sure you PLC running and the Profiler symbol is available at 'Profiler' or '{namespace}.Profiler'")
41 |
42 | # get header data
43 | tasks = plc.read_by_name(f"{profiler_symbolname}.Tasks", pyads.PLCTYPE_SINT)
44 | is_capturing = plc.read_by_name(f"{profiler_symbolname}.CaptureContinuous", pyads.PLCTYPE_BOOL)
45 | capturing_mode = plc.read_by_name(f"{profiler_symbolname}.Mode", pyads.PLCTYPE_INT)
46 | low_threshold = plc.read_by_name(f"{profiler_symbolname}.CaptureCpuTimeLowThreshold", pyads.PLCTYPE_LREAL)
47 | high_threshold = plc.read_by_name(f"{profiler_symbolname}.CaptureCpuTimeHighThreshold", pyads.PLCTYPE_LREAL)
48 | max_cycletime_in_s = max([plc.read_by_name(f"{profiler_symbolname}.CycleTime[{task}]", pyads.PLCTYPE_UDINT) for task in range(1, tasks+1)]) / 10000000.0
49 | pause_duration = 5 * max_cycletime_in_s
50 |
51 | # stop capturing
52 | plc.write_by_name(f"{profiler_symbolname}.CaptureOnce", False, pyads.PLCTYPE_BOOL)
53 | if is_capturing:
54 | plc.write_by_name(f"{profiler_symbolname}.CaptureContinuous", False, pyads.PLCTYPE_BOOL)
55 | logging.info(f"Capturing paused")
56 |
57 | # optionally reset previously taken frames
58 | if reset:
59 | logging.debug(f"Resetting profiler")
60 | trigger_edge(plc, f"{profiler_symbolname}.Reset", pause_duration)
61 |
62 | # optionally capture some frames
63 | if shots > 0:
64 | logging.debug(f"Temporarily configuring profiler for taking singleshots")
65 | plc.write_by_name(f"{profiler_symbolname}.Mode", 0, pyads.PLCTYPE_INT)
66 | plc.write_by_name(f"{profiler_symbolname}.CaptureCpuTimeLowThreshold", 0, pyads.PLCTYPE_LREAL)
67 | plc.write_by_name(f"{profiler_symbolname}.CaptureCpuTimeHighThreshold", 0, pyads.PLCTYPE_LREAL)
68 | time.sleep(pause_duration);
69 |
70 | for i in range(shots):
71 | logging.info(f"Taking snapshot {i+1}/{shots}")
72 | trigger_edge(plc, f"{profiler_symbolname}.CaptureOnce", pause_duration)
73 |
74 | time.sleep(pause_duration);
75 |
76 | # read all the data that the profile already captured
77 | max_stacksize = plc.read_by_name(f"{parameterlist_symbolname}.MAX_STACKSIZE", pyads.PLCTYPE_DINT)
78 | max_frames = plc.read_by_name(f"{parameterlist_symbolname}.MAX_FRAMES", pyads.PLCTYPE_SINT)
79 | frameIndex = plc.read_by_name(f"{profiler_symbolname}.FrameIndex", pyads.PLCTYPE_BYTE)
80 |
81 | logging.info(f"""Fetching callstacks from PLC with
82 | entrypoint = {profiler_symbolname}
83 | max_stacksize = {max_stacksize}
84 | max_frames = {max_frames}
85 | max_cycletime (s) = {max_cycletime_in_s}
86 | tasks = {tasks}""")
87 |
88 | common.create_stack_class(max_stacksize)
89 | counter = 0
90 | for task in range(1, tasks+1):
91 | cycletime = plc.read_by_name(f"{profiler_symbolname}.CycleTime[{task}]", pyads.PLCTYPE_UDINT)
92 |
93 | for frame in range(max_frames):
94 | stacksize = plc.read_by_name(f"{profiler_symbolname}.Meta[{frame}].Size", pyads.PLCTYPE_DINT)
95 |
96 | # abort if we don't get a valid stack out of it
97 | if stacksize > 0 and frame != frameIndex:
98 | stack = plc.read_by_name(f"{profiler_symbolname}.Data[{frame},{task}]", common.Stack)
99 | path = os.path.join(directory, f"{outputname}_frame_{counter}_task_{task}")
100 | callstacks.append(path)
101 | pickle.dump(common.Callstack(cycletime=cycletime, task=task, size=stacksize, stack=stack), open(callstacks[-1], "wb"))
102 | logging.info(f"Fetched Callstack {counter} (Task {task}) with calls {int(stacksize/2)} to {path}")
103 | counter += 1
104 |
105 | except pyads.ADSError as e:
106 | logging.error(e)
107 | finally:
108 | try:
109 | logging.debug(f"Reconfiguring profiler to initial setup")
110 | plc.write_by_name(f"{profiler_symbolname}.Mode", capturing_mode, pyads.PLCTYPE_INT)
111 | plc.write_by_name(f"{profiler_symbolname}.CaptureCpuTimeLowThreshold", low_threshold, pyads.PLCTYPE_LREAL)
112 | plc.write_by_name(f"{profiler_symbolname}.CaptureCpuTimeHighThreshold", high_threshold, pyads.PLCTYPE_LREAL)
113 | plc.write_by_name(f"{profiler_symbolname}.CaptureContinuous", is_capturing, pyads.PLCTYPE_BOOL)
114 | except:
115 | pass
116 | plc.close()
117 |
118 | return callstacks
119 |
120 |
121 |
--------------------------------------------------------------------------------
/pytwingrind/pytwingrind/prepare.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import os
5 | import re
6 | import logging
7 | import pickle
8 | from pytwingrind import common
9 |
10 | def create_hash(filepath, fb, method, hashes):
11 | increment = 0
12 | while True:
13 | hstr = filepath + "::" + fb + "::" + method + str(increment)
14 | h = hash(hstr) & 4294967295
15 | if h not in hashes:
16 | hashes[h] = (fb, method)
17 | return h
18 | increment += 1
19 |
20 | def add_guards(filepath, fb_name, hashes):
21 | """add guards to fb and all methods for this file"""
22 |
23 | src = ""
24 |
25 | try:
26 | with open(filepath, "rt", encoding=common.detect_encoding(filepath)) as f:
27 | src = f.read()
28 |
29 | # check if the file should not be guarded due to some token
30 | for t in common.file_skip_tokens:
31 | if t.token in src:
32 | logging.warning(f"Skipping {filepath}, {t.description}")
33 | return
34 |
35 | except UnicodeDecodeError as ex:
36 | print('File {} contains invalid characters, only ascii is supported'.format(filepath))
37 | raise ex
38 |
39 | nearly = 0
40 | ncallables = 0
41 |
42 | # add guards to functions
43 | functions = re.findall(r'<\/ST>', src, re.S | re.M | re.UNICODE)
44 | if functions:
45 | for m in functions:
46 | function_name = m[1]
47 | body = m[4]
48 | old_body = body
49 | hash = create_hash(filepath, fb_name, function_name, hashes)
50 |
51 | body = '''{tag}Twingrind.Profiler.Push({hash});{tag}\n'''.format(hash=hash, tag=common.profiler_tag) + body
52 | body, i = re.subn(r'RETURN([\s]*?);',
53 | r'''\1{tag}Twingrind.Profiler.Pop({hash});{tag}\1RETURN;'''.format(hash=hash, tag=common.profiler_tag),
54 | body, 0, re.S | re.M | re.UNICODE)
55 | body = body + '''\n{tag}Twingrind.Profiler.Pop({hash});{tag}'''.format(hash=hash, tag=common.profiler_tag)
56 |
57 | nearly += i # two guards are always added
58 | ncallables += 1
59 |
60 | src = src.replace(r''.format(spacer0=m[0],
61 | function_name=function_name,
62 | spacer2=m[2],
63 | spacer3=m[3],
64 | body=old_body),
65 | r''.format(spacer0=m[0],
66 | function_name=function_name,
67 | spacer2=m[2],
68 | spacer3=m[3],
69 | body=body))
70 |
71 | # add guards to programs
72 | programs = re.findall(r'<\/ST>', src, re.S | re.M | re.UNICODE)
73 | if programs:
74 | for m in programs:
75 | prg_name = m[1]
76 | body = m[4]
77 | old_body = body
78 | hash = create_hash(filepath, fb_name, prg_name, hashes)
79 |
80 | body = '''{tag}Twingrind.Profiler.Push({hash});{tag}\n'''.format(hash=hash, tag=common.profiler_tag) + body
81 | body, i = re.subn(r'RETURN([\s]*?);',
82 | r'''\1{tag}Twingrind.Profiler.Pop({hash});{tag}\1RETURN;'''.format(hash=hash, tag=common.profiler_tag),
83 | body, 0, re.S | re.M | re.UNICODE)
84 | body = body + '''\n{tag}Twingrind.Profiler.Pop({hash});{tag}'''.format(hash=hash, tag=common.profiler_tag)
85 |
86 | nearly += i # two guards are always added
87 | ncallables += 1
88 |
89 | src = src.replace(r''.format(spacer0=m[0],
90 | prg_name=prg_name,
91 | spacer2=m[2],
92 | spacer3=m[3],
93 | body=old_body),
94 | r''.format(spacer0=m[0],
95 | prg_name=prg_name,
96 | spacer2=m[2],
97 | spacer3=m[3],
98 | body=body))
99 |
100 | # add guards to function blocks
101 | functionblocks = re.findall(r'<\/ST>', src, re.S | re.M | re.UNICODE)
102 | if functionblocks:
103 | for m in functionblocks:
104 | functionblock_name = m[1]
105 | body = m[4]
106 | old_body = body
107 | hash = create_hash(filepath, fb_name, functionblock_name, hashes)
108 |
109 | body = '''{tag}Twingrind.Profiler.Push({hash});{tag}\n'''.format(hash=hash, tag=common.profiler_tag) + body
110 | body, i = re.subn(r'RETURN([\s]*?);',
111 | r'''\1{tag}Twingrind.Profiler.Pop({hash});{tag}RETURN\1;'''.format(hash=hash, tag=common.profiler_tag),
112 | body, 0, re.S | re.M | re.UNICODE)
113 | body = body + '''\n{tag}Twingrind.Profiler.Pop({hash});{tag}'''.format(hash=hash, tag=common.profiler_tag)
114 |
115 | nearly += i # two guards are always added
116 | ncallables += 1
117 |
118 | src = src.replace(r''.format(spacer0=m[0],
119 | functionblock_name=functionblock_name,
120 | spacer2=m[2],
121 | spacer3=m[3],
122 | body=old_body),
123 | r''.format(spacer0=m[0],
124 | functionblock_name=functionblock_name,
125 | spacer2=m[2],
126 | spacer3=m[3],
127 | body=body))
128 |
129 | # add guards to all methods
130 | methods = re.findall(r'<\/ST>', src, re.S | re.M | re.UNICODE)
131 | if methods:
132 | for m in methods:
133 | if ' ABSTRACT ' in m[2]:
134 | continue
135 |
136 | method_name = m[1]
137 | body = m[3]
138 | old_body = body
139 | hash = create_hash(filepath, fb_name, method_name, hashes)
140 |
141 | body = '''{tag}Twingrind.Profiler.Push({hash});{tag}\n'''.format(hash=hash, tag=common.profiler_tag) + body
142 | body, i = re.subn(r'RETURN([\s]*?);',
143 | r'''\1{tag}Twingrind.Profiler.Pop({hash});{tag}\1RETURN;'''.format(hash=hash, tag=common.profiler_tag),
144 | body, 0, re.S | re.M | re.UNICODE)
145 | body = body + '''\n{tag}Twingrind.Profiler.Pop({hash});{tag}'''.format(hash=hash, tag=common.profiler_tag)
146 |
147 | nearly += i # two guards are always added
148 | ncallables += 1
149 |
150 | src = src.replace(r''.format(spacer0=m[0],
151 | method_name=method_name,
152 | spacer2=m[2],
153 | body=old_body,
154 | fb=fb_name),
155 | r''.format(spacer0=m[0],
156 | method_name=method_name,
157 | spacer2=m[2],
158 | body=body,
159 | fb=fb_name))
160 |
161 | logging.debug("{}: added {} guards and covered {} paths".format(fb_name, ncallables, nearly+1))
162 |
163 | with open(filepath, "wt", encoding=common.detect_encoding(filepath)) as g:
164 | g.write(src)
165 |
166 |
167 |
168 | def run(filepath : str, hashmap : str):
169 | hashes = {}
170 |
171 | try:
172 | hashes = pickle.load(open(hashmap, 'rb'))
173 | logging.info('Updating an existing hashfile')
174 | except:
175 | logging.info('Creating a new hashfile')
176 |
177 | for f in common.find_sourcefiles(filepath):
178 | fb_name, _ = os.path.splitext(os.path.basename(f))
179 | add_guards(f, fb_name, hashes)
180 |
181 | pickle.dump(hashes, open(hashmap, "wb"))
182 | print('Hashmap location={}'.format(hashmap))
183 | print('Containing {} hashes'.format(len(hashes)))
184 | print('Do not forget to call Twingrind.Profiler() in the *first line* of the *first PRG* in the PLC task!')
185 | print('''
186 | MAIN.PRG
187 | -------------------------------
188 | 1 Twingrind.Profiler();
189 | 2
190 | 3 //
191 | 4 //
192 | 5 //
193 | .
194 | .
195 | ''')
196 |
--------------------------------------------------------------------------------
/pytwingrind/pytwingrind/reconstruct.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | import pickle
4 | import inspect
5 | import networkx
6 | import numpy as np
7 | import ctypes
8 | from pytwingrind import common
9 | from enum import IntEnum
10 | from pytwingrind.common import Call
11 |
12 |
13 | class StackRow(IntEnum):
14 | DEPTH = 0
15 | START_100NS = 1
16 | END_100NS = 2
17 | HASH = 3
18 |
19 |
20 | def extract_stack(stack, hashmap):
21 | data = np.zeros((len(stack.calls), 4), dtype=np.uint64)
22 | size = 0
23 | def hilo_to_lword(hi, lo): return ((hi << 32) + lo)
24 |
25 | for call in stack.calls:
26 | data[size] = [call.depth, hilo_to_lword(
27 | call.starthi, call.startlo), hilo_to_lword(call.endhi, call.endlo), call.hash]
28 |
29 | # no more valid timestamps
30 | if np.all(data[size] == 0):
31 | break;
32 | size = size + 1
33 |
34 | logging.info(f"Extracted {int(size/2)} calls")
35 | return data[0:size]
36 |
37 |
38 | def build_graph(network, hashmap, roots, data, sid=-1, eid=-1):
39 |
40 | if sid < 0 and eid < 0:
41 | sid = 0
42 | eid = len(data)
43 |
44 | endid = np.where(np.logical_and(data[sid+1:eid, StackRow.HASH] == data[sid, StackRow.HASH],
45 | data[sid+1:eid, StackRow.DEPTH] == data[sid, StackRow.DEPTH]))[0][0] + sid + 1
46 | dt_100ns = data[endid, StackRow.END_100NS] - data[sid, StackRow.START_100NS]
47 | fb, method = hashmap[data[endid, StackRow.HASH]] if hashmap is not None else (hex(data[endid, StackRow.HASH]), hex(data[endid, StackRow.HASH]))
48 | depth = int(data[endid, StackRow.DEPTH])+1
49 |
50 | roots = roots[0:depth]
51 | parent = roots[-1]
52 |
53 | if network.has_edge(parent, sid):
54 | network[parent][sid]['attr_dict']['calls'] += 1
55 | network[parent][sid]['attr_dict']['dt_100ns'] += [dt_100ns, ]
56 | else:
57 | network.add_edge(parent, sid, attr_dict={
58 | 'dt_100ns': [dt_100ns, ], 'calls': 1, 'name': '{}::{}'.format(fb, method)})
59 | if sid+1 != endid:
60 | build_graph(network, hashmap, roots + [sid], data, sid+1, endid)
61 |
62 | if(endid+1 < len(data)) and endid+1 != eid:
63 | build_graph(network, hashmap, roots, data, endid+1, eid)
64 |
65 |
66 | def write_callgrind(network, f, selfcost, node_start="root", node_name=None, depth=0):
67 |
68 | def ch(x, y): return x + '::' + y if len(y) > 0 else x
69 |
70 | # defaulting to cycle time 1 ms if nothing else is specified
71 | if selfcost < 0 and depth == 0:
72 | selfcost = 1000000000
73 | elif selfcost < 0:
74 | raise Exception('selfcost < 0')
75 |
76 | # write header information
77 | if depth == 0:
78 | f.write('events: dt')
79 | f.write('\nfl={}\n'.format(ch('Task', '')))
80 | f.write('fn={}\n'.format(ch('Task', 'Task')))
81 | f.write('{} {}\n'.format(1, int(selfcost))) # self cost
82 |
83 | # calculate self costs by substracting the costs of all calls
84 | for _, n in enumerate(network.neighbors(node_start)):
85 | for dt_100ns in network.get_edge_data(node_start, n)['attr_dict']['dt_100ns']:
86 | selfcost -= int(dt_100ns*100)
87 |
88 | if node_name is not None:
89 | node_fb, node_method = node_name.split('::')
90 | f.write('\nfl={}\n'.format(ch(node_fb, '')))
91 | f.write('fn={}\n'.format(ch(node_fb, node_method)))
92 | f.write('{} {}\n'.format(1, selfcost))
93 | for i, n in enumerate(network.neighbors(node_start)):
94 | fb, method = network.get_edge_data(
95 | node_start, n)['attr_dict']['name'].split('::')
96 |
97 | calls = network.get_edge_data(node_start, n)['attr_dict']['calls']
98 | dts = network.get_edge_data(node_start, n)['attr_dict']['dt_100ns']
99 |
100 | for c in range(calls):
101 | f.write('cfl={}\n'.format(ch(fb, '')))
102 | f.write('cfn={}\n'.format(ch(fb, method)))
103 | f.write('calls={} {}\n'.format(1, 1))
104 | f.write('{} {}\n'.format(i, int(dts[c]*100)))
105 |
106 | for i, n in enumerate(network.neighbors(node_start)):
107 | dt_100ns = network.get_edge_data(node_start, n)['attr_dict']['dt_100ns']
108 | n_name = network.get_edge_data(node_start, n)['attr_dict']['name']
109 | write_callgrind(network, f, selfcost=int(max(dt_100ns)*100),
110 | node_start=n, node_name=n_name, depth=depth+1)
111 |
112 |
113 | def run(hashmap: str, file: str, dest: str, outputname: str):
114 |
115 | logging.info(f"Reconstructing callstack {file}")
116 |
117 | # unpickling is tricky if the Stack class does not exist yet. The latter
118 | # occurs if we use 'twingrind reconstruct' instead of 'twingrind process'.
119 | # Lets create a "wrong" Stack class that can only hold 1 call, then use
120 | # load the file and use the size, which is stored there, to create
121 | # the correct Stack class
122 | common.create_stack_class(1)
123 | callstack = pickle.load(open(file, 'rb'))
124 | common.create_stack_class(callstack.size)
125 | callstack = pickle.load(open(file, 'rb'))
126 |
127 | logging.debug(f"Callstack size={callstack.size}")
128 |
129 | hm = None
130 | if hashmap is not None:
131 | hm = pickle.load(open(hashmap, 'rb'))
132 |
133 | data = extract_stack(callstack.stack, hm)
134 | n = networkx.DiGraph()
135 | build_graph(n, hm, ['root'], data)
136 |
137 | logging.info(f'Reconstructed {int(len(data) / 2)} calls')
138 | filename = os.path.join(
139 | dest, f"callgrind.{outputname}{os.path.basename(file)}")
140 | with open(filename, 'wt') as f:
141 | write_callgrind(n, f, int(callstack.cycletime * 100))
142 | logging.info(f'Reconstructed callgrind file to {filename}')
143 |
--------------------------------------------------------------------------------
/pytwingrind/requirements.txt:
--------------------------------------------------------------------------------
1 | pyads~=3.3.9
2 | networkx
3 | numpy
4 | chardet
5 |
--------------------------------------------------------------------------------
/pytwingrind/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from setuptools import setup, find_packages
4 | from os import environ
5 |
6 | setup(
7 | name="pytwingrind",
8 | version=f"0.4.1",
9 | author="Stefan Besler",
10 | author_email="stefan@besler.me",
11 | description="Call-graph profiling for TwinCAT 3.",
12 | long_description="Call-graph profiling for TwinCAT 3.",
13 | long_description_content_type="text/markdown",
14 | url="https://github.com/stefanbesler/twingrind",
15 | packages=find_packages(),
16 | classifiers=[
17 | "Programming Language :: Python :: 3",
18 | ],
19 | python_requires=">=3.8",
20 | entry_points={
21 | "console_scripts": [
22 | "twingrind = pytwingrind.__main__:main"
23 | ],
24 | },
25 | install_requires=list(open('requirements.txt')),
26 | )
27 |
--------------------------------------------------------------------------------
/pytwingrind/twingrind.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from pytwingrind.__main__ import main
4 |
5 | if __name__ == "__main__":
6 | main()
--------------------------------------------------------------------------------