Line data Source code
1 : /* Copyright (C) 2022 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "ProfileViewer.h"
21 :
22 : #include "graphics/Canvas2D.h"
23 : #include "graphics/FontMetrics.h"
24 : #include "graphics/TextRenderer.h"
25 : #include "lib/external_libraries/libsdl.h"
26 : #include "maths/Size2D.h"
27 : #include "maths/Vector2D.h"
28 : #include "ps/CLogger.h"
29 : #include "ps/CStrInternStatic.h"
30 : #include "ps/Filesystem.h"
31 : #include "ps/Hotkey.h"
32 : #include "ps/Profile.h"
33 : #include "ps/Pyrogenesis.h"
34 : #include "scriptinterface/Object.h"
35 :
36 : #include <algorithm>
37 : #include <ctime>
38 : #include <fstream>
39 :
40 6 : struct CProfileViewerInternals
41 : {
42 : NONCOPYABLE(CProfileViewerInternals); // because of the ofstream
43 : public:
44 6 : CProfileViewerInternals() {}
45 :
46 : /// Whether the profiling display is currently visible
47 : bool profileVisible;
48 :
49 : /// List of root tables
50 : std::vector<AbstractProfileTable*> rootTables;
51 :
52 : /// Path from a root table (path[0]) to the currently visible table (path[size-1])
53 : std::vector<AbstractProfileTable*> path;
54 :
55 : /// Helper functions
56 : void TableIsDeleted(AbstractProfileTable* table);
57 : void NavigateTree(int id);
58 :
59 : /// File for saved profile output (reset when the game is restarted)
60 : std::ofstream outputStream;
61 : };
62 :
63 :
64 : ///////////////////////////////////////////////////////////////////////////////////////////////
65 : // AbstractProfileTable implementation
66 :
67 12 : AbstractProfileTable::~AbstractProfileTable()
68 : {
69 6 : if (CProfileViewer::IsInitialised())
70 : {
71 6 : g_ProfileViewer.m->TableIsDeleted(this);
72 : }
73 6 : }
74 :
75 :
76 : ///////////////////////////////////////////////////////////////////////////////////////////////
77 : // CProfileViewer implementation
78 :
79 :
80 : // AbstractProfileTable got deleted, make sure we have no dangling pointers
81 6 : void CProfileViewerInternals::TableIsDeleted(AbstractProfileTable* table)
82 : {
83 12 : for(int idx = (int)rootTables.size()-1; idx >= 0; --idx)
84 : {
85 6 : if (rootTables[idx] == table)
86 6 : rootTables.erase(rootTables.begin() + idx);
87 : }
88 :
89 6 : for(size_t idx = 0; idx < path.size(); ++idx)
90 : {
91 0 : if (path[idx] != table)
92 0 : continue;
93 :
94 0 : path.erase(path.begin() + idx, path.end());
95 0 : if (path.size() == 0)
96 0 : profileVisible = false;
97 : }
98 6 : }
99 :
100 :
101 : // Move into child tables or return to parent tables based on the given number
102 0 : void CProfileViewerInternals::NavigateTree(int id)
103 : {
104 0 : if (id == 0)
105 : {
106 0 : if (path.size() > 1)
107 0 : path.pop_back();
108 : }
109 : else
110 : {
111 0 : AbstractProfileTable* table = path[path.size() - 1];
112 0 : size_t numrows = table->GetNumberRows();
113 :
114 0 : for(size_t row = 0; row < numrows; ++row)
115 : {
116 0 : AbstractProfileTable* child = table->GetChild(row);
117 :
118 0 : if (!child)
119 0 : continue;
120 :
121 0 : --id;
122 0 : if (id == 0)
123 : {
124 0 : path.push_back(child);
125 0 : break;
126 : }
127 : }
128 : }
129 0 : }
130 :
131 :
132 : // Construction/Destruction
133 6 : CProfileViewer::CProfileViewer()
134 : {
135 6 : m = new CProfileViewerInternals;
136 6 : m->profileVisible = false;
137 6 : }
138 :
139 12 : CProfileViewer::~CProfileViewer()
140 : {
141 6 : delete m;
142 6 : }
143 :
144 :
145 : // Render
146 0 : void CProfileViewer::RenderProfile(CCanvas2D& canvas)
147 : {
148 0 : if (!m->profileVisible)
149 0 : return;
150 :
151 0 : if (m->path.empty())
152 : {
153 0 : m->profileVisible = false;
154 0 : return;
155 : }
156 :
157 0 : PROFILE3_GPU("profile viewer");
158 :
159 0 : AbstractProfileTable* table = m->path[m->path.size() - 1];
160 0 : const std::vector<ProfileColumn>& columns = table->GetColumns();
161 0 : size_t numrows = table->GetNumberRows();
162 :
163 0 : CStrIntern font_name("mono-stroke-10");
164 0 : CFontMetrics font(font_name);
165 0 : int lineSpacing = font.GetLineSpacing();
166 :
167 : // Render background.
168 0 : float estimateWidth = 50.0f;
169 0 : for (const ProfileColumn& column : columns)
170 0 : estimateWidth += static_cast<float>(column.width);
171 :
172 0 : float estimateHeight = 3 + static_cast<float>(numrows);
173 0 : if (m->path.size() > 1)
174 0 : estimateHeight += 2;
175 0 : estimateHeight *= lineSpacing;
176 :
177 0 : canvas.DrawRect(CRect(CSize2D(estimateWidth, estimateHeight)), CColor(0.0f, 0.0f, 0.0f, 0.5f));
178 :
179 : // Draw row backgrounds.
180 0 : for (size_t row = 0; row < numrows; ++row)
181 : {
182 0 : canvas.DrawRect(
183 0 : CRect(CVector2D(0.0f, lineSpacing * (2.0f + row) + 2.0f), CSize2D(estimateWidth, lineSpacing)),
184 0 : row % 2 ? CColor(1.0f, 1.0f, 1.0f, 0.1f): CColor(0.0f, 0.0f, 0.0f, 0.1f));
185 : }
186 :
187 : // Print table and column titles.
188 0 : CTextRenderer textRenderer;
189 0 : textRenderer.SetCurrentFont(font_name);
190 0 : textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
191 0 : textRenderer.PrintfAt(2.0f, lineSpacing, L"%hs", table->GetTitle().c_str());
192 0 : textRenderer.Translate(22.0f, lineSpacing*2.0f);
193 :
194 0 : float colX = 0.0f;
195 0 : for (size_t col = 0; col < columns.size(); ++col)
196 : {
197 0 : CStrW text = columns[col].title.FromUTF8();
198 : int w, h;
199 0 : font.CalculateStringSize(text.c_str(), w, h);
200 :
201 0 : float x = colX;
202 0 : if (col > 0) // right-align all but the first column
203 0 : x += columns[col].width - w;
204 0 : textRenderer.Put(x, 0.0f, text.c_str());
205 :
206 0 : colX += columns[col].width;
207 : }
208 :
209 0 : textRenderer.Translate(0.0f, lineSpacing);
210 :
211 : // Print rows
212 0 : int currentExpandId = 1;
213 :
214 0 : for (size_t row = 0; row < numrows; ++row)
215 : {
216 0 : if (table->IsHighlightRow(row))
217 0 : textRenderer.SetCurrentColor(CColor(1.0f, 0.5f, 0.5f, 1.0f));
218 : else
219 0 : textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
220 :
221 0 : if (table->GetChild(row))
222 : {
223 0 : textRenderer.PrintfAt(-15.0f, 0.0f, L"%d", currentExpandId);
224 0 : currentExpandId++;
225 : }
226 :
227 0 : float rowColX = 0.0f;
228 0 : for (size_t col = 0; col < columns.size(); ++col)
229 : {
230 0 : CStrW text = table->GetCellText(row, col).FromUTF8();
231 : int w, h;
232 0 : font.CalculateStringSize(text.c_str(), w, h);
233 :
234 0 : float x = rowColX;
235 0 : if (col > 0) // right-align all but the first column
236 0 : x += columns[col].width - w;
237 0 : textRenderer.Put(x, 0.0f, text.c_str());
238 :
239 0 : rowColX += columns[col].width;
240 : }
241 :
242 0 : textRenderer.Translate(0.0f, lineSpacing);
243 : }
244 :
245 0 : textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 1.0f, 1.0f));
246 :
247 0 : if (m->path.size() > 1)
248 : {
249 0 : textRenderer.Translate(0.0f, lineSpacing);
250 0 : textRenderer.Put(-15.0f, 0.0f, L"0");
251 0 : textRenderer.Put(0.0f, 0.0f, L"back to parent");
252 : }
253 :
254 0 : canvas.DrawText(textRenderer);
255 : }
256 :
257 :
258 : // Handle input
259 0 : InReaction CProfileViewer::Input(const SDL_Event_* ev)
260 : {
261 0 : switch(ev->ev.type)
262 : {
263 0 : case SDL_KEYDOWN:
264 : {
265 0 : if (!m->profileVisible)
266 0 : break;
267 :
268 0 : int k = ev->ev.key.keysym.sym;
269 0 : if (k >= SDLK_0 && k <= SDLK_9)
270 : {
271 0 : m->NavigateTree(k - SDLK_0);
272 0 : return IN_HANDLED;
273 : }
274 0 : break;
275 : }
276 0 : case SDL_HOTKEYPRESS:
277 0 : std::string hotkey = static_cast<const char*>(ev->ev.user.data1);
278 :
279 0 : if( hotkey == "profile.toggle" )
280 : {
281 0 : if (!m->profileVisible)
282 : {
283 0 : if (m->rootTables.size())
284 : {
285 0 : m->profileVisible = true;
286 0 : m->path.push_back(m->rootTables[0]);
287 : }
288 : }
289 : else
290 : {
291 : size_t i;
292 :
293 0 : for(i = 0; i < m->rootTables.size(); ++i)
294 : {
295 0 : if (m->rootTables[i] == m->path[0])
296 0 : break;
297 : }
298 0 : i++;
299 :
300 0 : m->path.clear();
301 0 : if (i < m->rootTables.size())
302 : {
303 0 : m->path.push_back(m->rootTables[i]);
304 : }
305 : else
306 : {
307 0 : m->profileVisible = false;
308 : }
309 : }
310 0 : return( IN_HANDLED );
311 : }
312 0 : else if( hotkey == "profile.save" )
313 : {
314 0 : SaveToFile();
315 0 : return( IN_HANDLED );
316 : }
317 0 : break;
318 : }
319 0 : return( IN_PASS );
320 : }
321 :
322 7 : InReaction CProfileViewer::InputThunk(const SDL_Event_* ev)
323 : {
324 7 : if (CProfileViewer::IsInitialised())
325 0 : return g_ProfileViewer.Input(ev);
326 :
327 7 : return IN_PASS;
328 : }
329 :
330 :
331 : // Add a table to the list of roots
332 6 : void CProfileViewer::AddRootTable(AbstractProfileTable* table, bool front)
333 : {
334 6 : if (front)
335 0 : m->rootTables.insert(m->rootTables.begin(), table);
336 : else
337 6 : m->rootTables.push_back(table);
338 6 : }
339 :
340 : namespace
341 : {
342 : class WriteTable
343 : {
344 : public:
345 0 : WriteTable(std::ofstream& outputStream) : m_OutputStream(outputStream) {}
346 : WriteTable(const WriteTable& writeTable) = default;
347 :
348 0 : void operator() (AbstractProfileTable* table)
349 : {
350 0 : std::vector<CStr> data; // 2d array of (rows+head)*columns elements
351 :
352 0 : const std::vector<ProfileColumn>& columns = table->GetColumns();
353 :
354 : // Add column headers to 'data'
355 0 : for (std::vector<ProfileColumn>::const_iterator col_it = columns.begin();
356 0 : col_it != columns.end(); ++col_it)
357 0 : data.push_back(col_it->title);
358 :
359 : // Recursively add all profile data to 'data'
360 0 : WriteRows(1, table, data);
361 :
362 : // Calculate the width of each column ( = the maximum width of
363 : // any value in that column)
364 0 : std::vector<size_t> columnWidths;
365 0 : size_t cols = columns.size();
366 0 : for (size_t c = 0; c < cols; ++c)
367 : {
368 0 : size_t max = 0;
369 0 : for (size_t i = c; i < data.size(); i += cols)
370 0 : max = std::max(max, data[i].length());
371 0 : columnWidths.push_back(max);
372 : }
373 :
374 : // Output data as a formatted table:
375 :
376 0 : m_OutputStream << "\n\n" << table->GetTitle() << "\n";
377 :
378 0 : if (cols == 0) // avoid divide-by-zero
379 0 : return;
380 :
381 0 : for (size_t r = 0; r < data.size()/cols; ++r)
382 : {
383 0 : for (size_t c = 0; c < cols; ++c)
384 0 : m_OutputStream << (c ? " | " : "\n")
385 0 : << data[r*cols + c].Pad(PS_TRIM_RIGHT, columnWidths[c]);
386 :
387 : // Add dividers under some rows. (Currently only the first, since
388 : // that contains the column headers.)
389 0 : if (r == 0)
390 0 : for (size_t c = 0; c < cols; ++c)
391 0 : m_OutputStream << (c ? "-|-" : "\n")
392 0 : << CStr::Repeat("-", columnWidths[c]);
393 : }
394 : }
395 :
396 0 : void WriteRows(int indent, AbstractProfileTable* table, std::vector<CStr>& data)
397 : {
398 0 : const std::vector<ProfileColumn>& columns = table->GetColumns();
399 :
400 0 : for (size_t r = 0; r < table->GetNumberRows(); ++r)
401 : {
402 : // Do pretty tree-structure indenting
403 0 : CStr indentation = CStr::Repeat("| ", indent-1);
404 0 : if (r+1 == table->GetNumberRows())
405 0 : indentation += "'-";
406 : else
407 0 : indentation += "|-";
408 :
409 0 : for (size_t c = 0; c < columns.size(); ++c)
410 0 : if (c == 0)
411 0 : data.push_back(indentation + table->GetCellText(r, c));
412 : else
413 0 : data.push_back(table->GetCellText(r, c));
414 :
415 0 : if (table->GetChild(r))
416 0 : WriteRows(indent+1, table->GetChild(r), data);
417 : }
418 0 : }
419 :
420 : private:
421 : std::ofstream& m_OutputStream;
422 : const WriteTable& operator=(const WriteTable&);
423 : };
424 :
425 : struct DumpTable
426 : {
427 : const ScriptInterface& m_ScriptInterface;
428 : JS::PersistentRooted<JS::Value> m_Root;
429 : DumpTable(const ScriptInterface& scriptInterface, JS::HandleValue root) :
430 : m_ScriptInterface(scriptInterface)
431 : {
432 : ScriptRequest rq(scriptInterface);
433 : m_Root.init(rq.cx, root);
434 : }
435 :
436 : // std::for_each requires a move constructor and the use of JS::PersistentRooted<T> apparently breaks a requirement for an
437 : // automatic move constructor
438 : DumpTable(DumpTable && original) :
439 : m_ScriptInterface(original.m_ScriptInterface)
440 : {
441 : ScriptRequest rq(m_ScriptInterface);
442 : m_Root.init(rq.cx, original.m_Root.get());
443 : }
444 :
445 : void operator() (AbstractProfileTable* table)
446 : {
447 : ScriptRequest rq(m_ScriptInterface);
448 :
449 : JS::RootedValue t(rq.cx);
450 : Script::CreateObject(
451 : rq,
452 : &t,
453 : "cols", DumpCols(table),
454 : "data", DumpRows(table));
455 :
456 : Script::SetProperty(rq, m_Root, table->GetTitle().c_str(), t);
457 : }
458 :
459 : std::vector<std::string> DumpCols(AbstractProfileTable* table)
460 : {
461 : std::vector<std::string> titles;
462 :
463 : const std::vector<ProfileColumn>& columns = table->GetColumns();
464 :
465 : for (size_t c = 0; c < columns.size(); ++c)
466 : titles.push_back(columns[c].title);
467 :
468 : return titles;
469 : }
470 :
471 : JS::Value DumpRows(AbstractProfileTable* table)
472 : {
473 : ScriptRequest rq(m_ScriptInterface);
474 :
475 : JS::RootedValue data(rq.cx);
476 : Script::CreateObject(rq, &data);
477 :
478 : const std::vector<ProfileColumn>& columns = table->GetColumns();
479 :
480 : for (size_t r = 0; r < table->GetNumberRows(); ++r)
481 : {
482 : JS::RootedValue row(rq.cx);
483 : Script::CreateArray(rq, &row);
484 :
485 : Script::SetProperty(rq, data, table->GetCellText(r, 0).c_str(), row);
486 :
487 : if (table->GetChild(r))
488 : {
489 : JS::RootedValue childRows(rq.cx, DumpRows(table->GetChild(r)));
490 : Script::SetPropertyInt(rq, row, 0, childRows);
491 : }
492 :
493 : for (size_t c = 1; c < columns.size(); ++c)
494 : Script::SetPropertyInt(rq, row, c, table->GetCellText(r, c));
495 : }
496 :
497 : return data;
498 : }
499 :
500 : private:
501 : const DumpTable& operator=(const DumpTable&);
502 : };
503 :
504 0 : bool SortByName(AbstractProfileTable* a, AbstractProfileTable* b)
505 : {
506 0 : return (a->GetName() < b->GetName());
507 : }
508 : }
509 :
510 0 : void CProfileViewer::SaveToFile()
511 : {
512 : // Open the file, if necessary. If this method is called several times,
513 : // the profile results will be appended to the previous ones from the same
514 : // run.
515 0 : if (! m->outputStream.is_open())
516 : {
517 : // Open the file. (It will be closed when the CProfileViewer
518 : // destructor is called.)
519 0 : OsPath path = psLogDir()/"profile.txt";
520 0 : m->outputStream.open(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
521 :
522 0 : if (m->outputStream.fail())
523 : {
524 0 : LOGERROR("Failed to open profile log file");
525 0 : return;
526 : }
527 : else
528 : {
529 0 : LOGMESSAGERENDER("Profiler snapshot saved to '%s'", path.string8());
530 : }
531 : }
532 :
533 : time_t t;
534 0 : time(&t);
535 0 : m->outputStream << "================================================================\n\n";
536 0 : m->outputStream << "PS profiler snapshot - " << asctime(localtime(&t));
537 :
538 0 : std::vector<AbstractProfileTable*> tables = m->rootTables;
539 0 : sort(tables.begin(), tables.end(), SortByName);
540 0 : for_each(tables.begin(), tables.end(), WriteTable(m->outputStream));
541 :
542 0 : m->outputStream << "\n\n================================================================\n";
543 0 : m->outputStream.flush();
544 : }
545 :
546 0 : void CProfileViewer::ShowTable(const CStr& table)
547 : {
548 0 : m->path.clear();
549 :
550 0 : if (table.length() > 0)
551 : {
552 0 : for (size_t i = 0; i < m->rootTables.size(); ++i)
553 : {
554 0 : if (m->rootTables[i]->GetName() == table)
555 : {
556 0 : m->path.push_back(m->rootTables[i]);
557 0 : m->profileVisible = true;
558 0 : return;
559 : }
560 : }
561 : }
562 :
563 : // No matching table found, so don't display anything
564 0 : m->profileVisible = false;
565 3 : }
|