1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
use Slim\Slim; |
4
|
|
|
|
5
|
|
|
class Xhgui_Controller_Run extends Xhgui_Controller |
6
|
|
|
{ |
7
|
|
|
/** |
8
|
|
|
* HTTP GET attribute name for comma separated filters |
9
|
|
|
*/ |
10
|
|
|
const FILTER_ARGUMENT_NAME = 'filter'; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* @var Xhgui_Searcher_Interface |
14
|
|
|
*/ |
15
|
|
|
private $searcher; |
16
|
|
|
|
17
|
|
|
public function __construct(Slim $app, Xhgui_Searcher_Interface $searcher) |
18
|
|
|
{ |
19
|
|
|
parent::__construct($app); |
20
|
|
|
$this->searcher = $searcher; |
21
|
|
|
} |
22
|
|
|
|
23
|
|
|
public function index() |
24
|
|
|
{ |
25
|
|
|
$response = $this->app->response(); |
26
|
|
|
// The list changes whenever new profiles are recorded. |
27
|
|
|
// Generally avoid caching, but allow re-use in browser's bfcache |
28
|
|
|
// and by cache proxies for concurrent requests. |
29
|
|
|
// https://github.com/perftools/xhgui/issues/261 |
30
|
|
|
$response->headers->set('Cache-Control', 'public, max-age=0'); |
31
|
|
|
|
32
|
|
|
$request = $this->app->request(); |
33
|
|
|
|
34
|
|
|
$search = array(); |
35
|
|
|
$keys = array('date_start', 'date_end', 'url'); |
36
|
|
|
foreach ($keys as $key) { |
37
|
|
|
if ($request->get($key)) { |
38
|
|
|
$search[$key] = $request->get($key); |
39
|
|
|
} |
40
|
|
|
} |
41
|
|
|
$sort = $request->get('sort'); |
42
|
|
|
|
43
|
|
|
$result = $this->searcher->getAll(array( |
44
|
|
|
'sort' => $sort, |
45
|
|
|
'page' => $request->get('page'), |
46
|
|
|
'direction' => $request->get('direction'), |
47
|
|
|
'perPage' => $this->app->config('page.limit'), |
48
|
|
|
'conditions' => $search, |
49
|
|
|
'projection' => true, |
50
|
|
|
)); |
51
|
|
|
|
52
|
|
|
$title = 'Recent runs'; |
53
|
|
|
$titleMap = array( |
54
|
|
|
'wt' => 'Longest wall time', |
55
|
|
|
'cpu' => 'Most CPU time', |
56
|
|
|
'mu' => 'Highest memory use', |
57
|
|
|
); |
58
|
|
|
if (isset($titleMap[$sort])) { |
59
|
|
|
$title = $titleMap[$sort]; |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
$paging = array( |
63
|
|
|
'total_pages' => $result['totalPages'], |
64
|
|
|
'page' => $result['page'], |
65
|
|
|
'sort' => $sort, |
66
|
|
|
'direction' => $result['direction'] |
67
|
|
|
); |
68
|
|
|
|
69
|
|
|
$this->_template = 'runs/list.twig'; |
70
|
|
|
$this->set(array( |
71
|
|
|
'paging' => $paging, |
72
|
|
|
'base_url' => 'home', |
73
|
|
|
'runs' => $result['results'], |
74
|
|
|
'date_format' => $this->app->config('date.format'), |
75
|
|
|
'search' => $search, |
76
|
|
|
'has_search' => strlen(implode('', $search)) > 0, |
77
|
|
|
'title' => $title |
78
|
|
|
)); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
public function view() |
82
|
|
|
{ |
83
|
|
|
$response = $this->app->response(); |
84
|
|
|
// Permalink views to a specific run are meant to be public and immutable. |
85
|
|
|
// But limit the cache to only a short period of time (enough to allow |
86
|
|
|
// handling of abuse or other stampedes). This way we don't have to |
87
|
|
|
// deal with any kind of purging system for when profiles are deleted, |
88
|
|
|
// or for after XHGui itself is upgraded and static assets may be |
89
|
|
|
// incompatible etc. |
90
|
|
|
// https://github.com/perftools/xhgui/issues/261 |
91
|
|
|
$response->headers->set('Cache-Control', 'public, max-age=60, must-revalidate'); |
92
|
|
|
|
93
|
|
|
$request = $this->app->request(); |
94
|
|
|
$detailCount = $this->app->config('detail.count'); |
95
|
|
|
$result = $this->searcher->get($request->get('id')); |
96
|
|
|
|
97
|
|
|
$result->calculateSelf(); |
98
|
|
|
|
99
|
|
|
// Self wall time graph |
100
|
|
|
$timeChart = $result->extractDimension('ewt', $detailCount); |
101
|
|
|
|
102
|
|
|
// Memory Block |
103
|
|
|
$memoryChart = $result->extractDimension('emu', $detailCount); |
104
|
|
|
|
105
|
|
|
// Watched Functions Block |
106
|
|
|
$watchedFunctions = array(); |
107
|
|
|
foreach ($this->searcher->getAllWatches() as $watch) { |
108
|
|
|
$matches = $result->getWatched($watch['name']); |
109
|
|
|
if ($matches) { |
110
|
|
|
$watchedFunctions = array_merge($watchedFunctions, $matches); |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
if (false !== $request->get(self::FILTER_ARGUMENT_NAME, false)) { |
115
|
|
|
$profile = $result->sort('ewt', $result->filter($result->getProfile(), $this->getFilters())); |
116
|
|
|
} else { |
117
|
|
|
$profile = $result->sort('ewt', $result->getProfile()); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$this->_template = 'runs/view.twig'; |
121
|
|
|
$this->set(array( |
122
|
|
|
'profile' => $profile, |
123
|
|
|
'result' => $result, |
124
|
|
|
'wall_time' => $timeChart, |
125
|
|
|
'memory' => $memoryChart, |
126
|
|
|
'watches' => $watchedFunctions, |
127
|
|
|
'date_format' => $this->app->config('date.format'), |
128
|
|
|
)); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* @return array |
133
|
|
|
*/ |
134
|
|
|
protected function getFilters() |
135
|
|
|
{ |
136
|
|
|
$request = $this->app->request(); |
137
|
|
|
$filterString = $request->get(self::FILTER_ARGUMENT_NAME); |
138
|
|
|
if (strlen($filterString) > 1 && $filterString !== 'true') { |
139
|
|
|
$filters = array_map('trim', explode(',', $filterString)); |
140
|
|
|
} else { |
141
|
|
|
$filters = $this->app->config('run.view.filter.names'); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
return $filters; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
public function deleteForm() |
148
|
|
|
{ |
149
|
|
|
$request = $this->app->request(); |
150
|
|
|
$id = $request->get('id'); |
151
|
|
|
if (!is_string($id) || !strlen($id)) { |
152
|
|
|
throw new Exception('The "id" parameter is required.'); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
// Get details |
156
|
|
|
$result = $this->searcher->get($id); |
157
|
|
|
|
158
|
|
|
$this->_template = 'runs/delete-form.twig'; |
159
|
|
|
$this->set(array( |
160
|
|
|
'run_id' => $id, |
161
|
|
|
'result' => $result, |
162
|
|
|
)); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
public function deleteSubmit() |
166
|
|
|
{ |
167
|
|
|
$request = $this->app->request(); |
168
|
|
|
$id = $request->post('id'); |
169
|
|
|
// Don't call profilers->delete() unless $id is set, |
170
|
|
|
// otherwise it will turn the null into a MongoId and return "Sucessful". |
171
|
|
|
if (!is_string($id) || !strlen($id)) { |
172
|
|
|
// Form checks this already, |
173
|
|
|
// only reachable by handcrafted or malformed requests. |
174
|
|
|
throw new Exception('The "id" parameter is required.'); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
// Delete the profile run. |
178
|
|
|
$this->searcher->delete($id); |
179
|
|
|
|
180
|
|
|
$this->app->flash('success', 'Deleted profile ' . $id); |
181
|
|
|
|
182
|
|
|
$this->app->redirect($this->app->urlFor('home')); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
public function deleteAllForm() |
186
|
|
|
{ |
187
|
|
|
$this->_template = 'runs/delete-all-form.twig'; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
public function deleteAllSubmit() |
191
|
|
|
{ |
192
|
|
|
// Delete all profile runs. |
193
|
|
|
$this->searcher->truncate(); |
194
|
|
|
|
195
|
|
|
$this->app->flash('success', 'Deleted all profiles'); |
196
|
|
|
|
197
|
|
|
$this->app->redirect($this->app->urlFor('home')); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
public function url() |
201
|
|
|
{ |
202
|
|
|
$request = $this->app->request(); |
203
|
|
|
$pagination = array( |
204
|
|
|
'sort' => $request->get('sort'), |
205
|
|
|
'direction' => $request->get('direction'), |
206
|
|
|
'page' => $request->get('page'), |
207
|
|
|
'perPage' => $this->app->config('page.limit'), |
208
|
|
|
); |
209
|
|
|
|
210
|
|
|
$search = array(); |
211
|
|
|
$keys = array('date_start', 'date_end', 'limit', 'limit_custom'); |
212
|
|
|
foreach ($keys as $key) { |
213
|
|
|
$search[$key] = $request->get($key); |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
$runs = $this->searcher->getForUrl( |
217
|
|
|
$request->get('url'), |
218
|
|
|
$pagination, |
219
|
|
|
$search |
220
|
|
|
); |
221
|
|
|
|
222
|
|
|
if (isset($search['limit_custom']) && |
223
|
|
|
strlen($search['limit_custom']) > 0 && |
224
|
|
|
$search['limit_custom'][0] === 'P' |
225
|
|
|
) { |
226
|
|
|
$search['limit'] = $search['limit_custom']; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
$chartData = $this->searcher->getPercentileForUrl( |
230
|
|
|
90, |
231
|
|
|
$request->get('url'), |
232
|
|
|
$search |
233
|
|
|
); |
234
|
|
|
|
235
|
|
|
$paging = array( |
236
|
|
|
'total_pages' => $runs['totalPages'], |
237
|
|
|
'sort' => $pagination['sort'], |
238
|
|
|
'page' => $runs['page'], |
239
|
|
|
'direction' => $runs['direction'] |
240
|
|
|
); |
241
|
|
|
|
242
|
|
|
$this->_template = 'runs/url.twig'; |
243
|
|
|
$this->set(array( |
244
|
|
|
'paging' => $paging, |
245
|
|
|
'base_url' => 'url.view', |
246
|
|
|
'runs' => $runs['results'], |
247
|
|
|
'url' => $request->get('url'), |
248
|
|
|
'chart_data' => $chartData, |
249
|
|
|
'date_format' => $this->app->config('date.format'), |
250
|
|
|
'search' => array_merge($search, array('url' => $request->get('url'))), |
251
|
|
|
)); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
public function compare() |
255
|
|
|
{ |
256
|
|
|
$request = $this->app->request(); |
257
|
|
|
|
258
|
|
|
$baseRun = $headRun = $candidates = $comparison = null; |
259
|
|
|
$paging = array(); |
260
|
|
|
|
261
|
|
|
if ($request->get('base')) { |
262
|
|
|
$baseRun = $this->searcher->get($request->get('base')); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
if ($baseRun && !$request->get('head')) { |
266
|
|
|
$pagination = array( |
267
|
|
|
'direction' => $request->get('direction'), |
268
|
|
|
'sort' => $request->get('sort'), |
269
|
|
|
'page' => $request->get('page'), |
270
|
|
|
'perPage' => $this->app->config('page.limit'), |
271
|
|
|
); |
272
|
|
|
$candidates = $this->searcher->getForUrl( |
273
|
|
|
$baseRun->getMeta('simple_url'), |
274
|
|
|
$pagination |
275
|
|
|
); |
276
|
|
|
|
277
|
|
|
$paging = array( |
278
|
|
|
'total_pages' => $candidates['totalPages'], |
279
|
|
|
'sort' => $pagination['sort'], |
280
|
|
|
'page' => $candidates['page'], |
281
|
|
|
'direction' => $candidates['direction'] |
282
|
|
|
); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
if ($request->get('head')) { |
286
|
|
|
$headRun = $this->searcher->get($request->get('head')); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
if ($baseRun && $headRun) { |
290
|
|
|
$comparison = $baseRun->compare($headRun); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
$this->_template = 'runs/compare.twig'; |
294
|
|
|
$this->set(array( |
295
|
|
|
'base_url' => 'run.compare', |
296
|
|
|
'base_run' => $baseRun, |
297
|
|
|
'head_run' => $headRun, |
298
|
|
|
'candidates' => $candidates, |
299
|
|
|
'url_params' => $request->get(), |
300
|
|
|
'date_format' => $this->app->config('date.format'), |
301
|
|
|
'comparison' => $comparison, |
302
|
|
|
'paging' => $paging, |
303
|
|
|
'search' => array( |
304
|
|
|
'base' => $request->get('base'), |
305
|
|
|
'head' => $request->get('head'), |
306
|
|
|
) |
307
|
|
|
)); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
public function symbol() |
311
|
|
|
{ |
312
|
|
|
$request = $this->app->request(); |
313
|
|
|
$id = $request->get('id'); |
314
|
|
|
$symbol = $request->get('symbol'); |
315
|
|
|
|
316
|
|
|
$profile = $this->searcher->get($id); |
317
|
|
|
$profile->calculateSelf(); |
318
|
|
|
list($parents, $current, $children) = $profile->getRelatives($symbol); |
319
|
|
|
|
320
|
|
|
$this->_template = 'runs/symbol.twig'; |
321
|
|
|
$this->set(array( |
322
|
|
|
'symbol' => $symbol, |
323
|
|
|
'id' => $id, |
324
|
|
|
'main' => $profile->get('main()'), |
325
|
|
|
'parents' => $parents, |
326
|
|
|
'current' => $current, |
327
|
|
|
'children' => $children, |
328
|
|
|
)); |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
public function symbolShort() |
332
|
|
|
{ |
333
|
|
|
$request = $this->app->request(); |
334
|
|
|
$id = $request->get('id'); |
335
|
|
|
$threshold = $request->get('threshold'); |
336
|
|
|
$symbol = $request->get('symbol'); |
337
|
|
|
$metric = $request->get('metric'); |
338
|
|
|
|
339
|
|
|
$profile = $this->searcher->get($id); |
340
|
|
|
$profile->calculateSelf(); |
341
|
|
|
list($parents, $current, $children) = $profile->getRelatives($symbol, $metric, $threshold); |
342
|
|
|
|
343
|
|
|
$this->_template = 'runs/symbol-short.twig'; |
344
|
|
|
$this->set(array( |
345
|
|
|
'symbol' => $symbol, |
346
|
|
|
'id' => $id, |
347
|
|
|
'main' => $profile->get('main()'), |
348
|
|
|
'parents' => $parents, |
349
|
|
|
'current' => $current, |
350
|
|
|
'children' => $children, |
351
|
|
|
)); |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
public function callgraph() |
355
|
|
|
{ |
356
|
|
|
$request = $this->app->request(); |
357
|
|
|
$profile = $this->searcher->get($request->get('id')); |
358
|
|
|
|
359
|
|
|
$this->_template = 'runs/callgraph.twig'; |
360
|
|
|
$this->set(array( |
361
|
|
|
'profile' => $profile, |
362
|
|
|
'date_format' => $this->app->config('date.format'), |
363
|
|
|
)); |
364
|
|
|
} |
365
|
|
|
|
366
|
|
View Code Duplication |
public function callgraphData() |
|
|
|
|
367
|
|
|
{ |
368
|
|
|
$request = $this->app->request(); |
369
|
|
|
$response = $this->app->response(); |
370
|
|
|
$profile = $this->searcher->get($request->get('id')); |
371
|
|
|
$metric = $request->get('metric') ?: 'wt'; |
372
|
|
|
$threshold = (float)$request->get('threshold') ?: 0.01; |
373
|
|
|
$callgraph = $profile->getCallgraph($metric, $threshold); |
374
|
|
|
|
375
|
|
|
$response['Content-Type'] = 'application/json'; |
376
|
|
|
return $response->body(json_encode($callgraph)); |
|
|
|
|
377
|
|
|
} |
378
|
|
|
|
379
|
|
View Code Duplication |
public function callgraphDataDot() |
|
|
|
|
380
|
|
|
{ |
381
|
|
|
$request = $this->app->request(); |
382
|
|
|
$response = $this->app->response(); |
383
|
|
|
$profile = $this->searcher->get($request->get('id')); |
384
|
|
|
$metric = $request->get('metric') ?: 'wt'; |
385
|
|
|
$threshold = (float)$request->get('threshold') ?: 0.01; |
386
|
|
|
$callgraph = $profile->getCallgraphNodes($metric, $threshold); |
|
|
|
|
387
|
|
|
|
388
|
|
|
$response['Content-Type'] = 'application/json'; |
389
|
|
|
return $response->body(json_encode($callgraph)); |
|
|
|
|
390
|
|
|
} |
391
|
|
|
} |
392
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.