ImGuiTreemap.cpp 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #define PROFILER_IMGUI_DLL
  9. #include "ImGuiTreemapImpl.h"
  10. #include <AzCore/Debug/Trace.h>
  11. #include <AzCore/std/sort.h>
  12. #include <AzCore/std/parallel/scoped_lock.h>
  13. #include <AzCore/Math/Crc.h>
  14. #include <imgui/imgui.h>
  15. #include <AzCore/std/containers/unordered_map.h>
  16. #include <AzCore/std/smart_ptr/unique_ptr.h>
  17. using AZ::Name;
  18. #define CHECK_STACK_EMPTY() \
  19. AZ_Assert( \
  20. m_stack.empty(), \
  21. "The treemap stack was empty at the start of a traversal. This indicates a bug in the treemap implementation, so please file a " \
  22. "ticket and/or notify sig-core.")
  23. namespace Profiler
  24. {
  25. static void NameToHueSaturation(AZ::Name name, float& outHue, float& outSaturation)
  26. {
  27. uint32_t hash = AZ::Crc32{ name.GetStringView() };
  28. // Apply one round of LCG (constants from glibc)
  29. hash *= 1103515245u;
  30. hash += 12345u;
  31. // The explicit cast of the uint32 max quantity is needed because on cast, the value increases by 1.
  32. outHue = static_cast<float>(hash) / static_cast<float>(0xffffffff);
  33. outSaturation = 0.7f;
  34. }
  35. ImGuiTreemap& ImGuiTreemapFactoryImpl::Create(AZ::Name name, const char* unitLabel)
  36. {
  37. AZStd::scoped_lock lock{ m_mutex };
  38. AZ_Assert(!m_treemaps.contains(name), "Attempting to create treemap %s but it already exists!", name.GetCStr());
  39. ImGuiTreemap& treemap = m_treemaps.emplace(name).first->second;
  40. treemap.SetName(AZStd::move(name));
  41. treemap.SetUnitLabel(unitLabel);
  42. return treemap;
  43. }
  44. void ImGuiTreemapFactoryImpl::Destroy(ImGuiTreemap* treemap)
  45. {
  46. AZStd::scoped_lock lock{ m_mutex };
  47. AZ_Assert(
  48. m_treemaps.contains(treemap->GetName()), "Attempting to destroy treemap %s but it doesn't exist!",
  49. treemap->GetName().GetCStr());
  50. m_treemaps.erase(treemap->GetName());
  51. }
  52. const Name& ImGuiTreemapImpl::GetName() const
  53. {
  54. return m_name;
  55. }
  56. void ImGuiTreemapImpl::SetName(char const* name)
  57. {
  58. m_name = Name{ name };
  59. }
  60. void ImGuiTreemapImpl::SetName(Name name)
  61. {
  62. m_name = AZStd::move(name);
  63. }
  64. void ImGuiTreemapImpl::SetUnitLabel(const char* unitLabel)
  65. {
  66. m_unitLabel = unitLabel;
  67. }
  68. void ImGuiTreemapImpl::AddMask(const char* label, uint32_t mask)
  69. {
  70. m_masks[label] = mask;
  71. }
  72. void ImGuiTreemapImpl::SetRoots(AZStd::vector<TreemapNode>&& roots)
  73. {
  74. m_root.m_children = AZStd::move(roots);
  75. m_groups.clear();
  76. m_dirty = true;
  77. // Reset extent to trigger re-layout
  78. m_lastExtent[0] = 0;
  79. m_selectedNode = nullptr;
  80. m_currentMask = 0xffffffff;
  81. }
  82. void ImGuiTreemapImpl::Render(int x, int y, int w, int h)
  83. {
  84. const char* treemapName = m_name.IsEmpty() ? "Unnamed treemap" : m_name.GetCStr();
  85. if (m_root.m_children.empty())
  86. {
  87. ImGui::Begin(treemapName);
  88. ImGui::End();
  89. return;
  90. }
  91. ImVec2 offset{ static_cast<float>(x), static_cast<float>(y) };
  92. ImGui::SetNextWindowPos(offset, ImGuiCond_Once);
  93. ImVec2 extent{ static_cast<float>(w), static_cast<float>(h) };
  94. ImGui::SetNextWindowSize(extent, ImGuiCond_Once);
  95. if (ImGui::Begin(treemapName))
  96. {
  97. ImGui::Text("Total weight: %f %s", m_root.m_weight, m_unitLabel);
  98. if (!m_groups.empty())
  99. {
  100. ImGui::Text("Highlight Group");
  101. }
  102. for (auto& group : m_groups)
  103. {
  104. ImGui::SameLine();
  105. ImGui::Checkbox(group.first.GetCStr(), &group.second.m_active);
  106. }
  107. if (!m_masks.empty())
  108. {
  109. if (ImGui::RadioButton("Display All", m_currentMask == 0xffffffff))
  110. {
  111. m_currentMask = 0xffffffff;
  112. m_dirty = true;
  113. m_lastExtent[0] = 0;
  114. m_selectedNode = nullptr;
  115. }
  116. for (auto& mask : m_masks)
  117. {
  118. ImGui::SameLine();
  119. if (ImGui::RadioButton(mask.first, m_currentMask == mask.second))
  120. {
  121. m_currentMask = mask.second;
  122. m_dirty = true;
  123. m_lastExtent[0] = 0;
  124. m_selectedNode = nullptr;
  125. }
  126. }
  127. }
  128. if (m_selectedNode)
  129. {
  130. ImGui::Text(
  131. "Selected node: %s (%f %s)", m_selectedNode->m_name.IsEmpty() ? "[unnamed]" : m_selectedNode->m_name.GetCStr(),
  132. m_selectedNode->m_weight, m_unitLabel);
  133. }
  134. else
  135. {
  136. ImGui::Text("No node selected");
  137. }
  138. ImGui::Separator();
  139. ImDrawList* drawList = ImGui::GetWindowDrawList();
  140. const auto [cx, cy] = ImGui::GetCursorPos();
  141. const auto [wx, wy] = ImGui::GetWindowPos();
  142. const auto [ww, wh] = ImGui::GetWindowSize();
  143. bool focused = ImGui::IsWindowFocused();
  144. // Add 20 pixel gutter to bottom
  145. float treemapOffset[2] = { cx + wx, cy + wy };
  146. WeighAndComputeLayout(static_cast<int>(ww - cx), static_cast<int>(wh - cy) - 20);
  147. const auto [mx, my] = ImGui::GetMousePos();
  148. m_tooltipNodes.clear();
  149. // Draw nodes starting at the top level (ignoring the root) and descend down
  150. for (auto it = m_levelSets.begin() + 1; it != m_levelSets.end(); ++it)
  151. {
  152. for (TreemapNode* node : *it)
  153. {
  154. if (node->m_area < 1e-5 || (node->m_tag != 0 && (node->m_tag & m_currentMask) == 0))
  155. {
  156. continue;
  157. }
  158. ImVec2 topLeft{ static_cast<float>(node->m_offset[0]) + treemapOffset[0], static_cast<float>(node->m_offset[1]) + treemapOffset[1] };
  159. ImVec2 nodeExtent{ static_cast<float>(node->m_extent[0]), static_cast<float>(node->m_extent[1]) };
  160. if (node->m_children.empty())
  161. {
  162. // Shrink leaf nodes with an additional 2 pixel gutter
  163. topLeft.x += 2;
  164. topLeft.y += 2;
  165. nodeExtent.x -= 4;
  166. nodeExtent.y -= 4;
  167. }
  168. ImVec2 bottomRight{ topLeft.x + nodeExtent.x, topLeft.y + nodeExtent.y };
  169. float r;
  170. float g;
  171. float b;
  172. float saturationShift = 0.f;
  173. if (focused && mx > topLeft.x && mx < bottomRight.x && my > topLeft.y && my < bottomRight.y)
  174. {
  175. // Mouse is hovering over this node. Add it as a node to display in the tooltip
  176. saturationShift += 0.15f;
  177. m_tooltipNodes.push_back(node);
  178. if (focused && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && node->m_children.empty())
  179. {
  180. if (m_selectedNode == node)
  181. {
  182. m_selectedNode = nullptr;
  183. }
  184. else
  185. {
  186. m_selectedNode = node;
  187. }
  188. }
  189. }
  190. bool selected = m_selectedNode == node;
  191. if (!node->m_group.IsEmpty() && m_groups[node->m_group].m_active)
  192. {
  193. const GroupConfig& groupConfig = m_groups[node->m_group];
  194. ImGui::ColorConvertHSVtoRGB(
  195. groupConfig.m_hue,
  196. groupConfig.m_saturation + saturationShift,
  197. (selected ? 0.9f : node->m_value) + 0.1f,
  198. r, g, b);
  199. }
  200. else
  201. {
  202. ImGui::ColorConvertHSVtoRGB(
  203. node->m_hue, node->m_saturation + saturationShift, selected ? 1.f : node->m_value, r, g, b);
  204. }
  205. drawList->AddRectFilled(topLeft, bottomRight, ImColor{ r, g, b }, 2.f, ImDrawFlags_RoundCornersAll);
  206. }
  207. }
  208. if (!m_tooltipNodes.empty())
  209. {
  210. ImGui::BeginTooltip();
  211. for (TreemapNode* node : m_tooltipNodes)
  212. {
  213. if (!node->m_tooltip.empty())
  214. {
  215. ImGui::Text("%s", node->m_tooltip.c_str());
  216. }
  217. else if (!node->m_name.IsEmpty())
  218. {
  219. ImGui::Text("%s (%f %s)", node->m_name.GetCStr(), node->m_weight, m_unitLabel);
  220. }
  221. else
  222. {
  223. ImGui::Text("[unnamed] (%f %s)", node->m_weight, m_unitLabel);
  224. }
  225. ImGui::Indent();
  226. }
  227. ImGui::EndTooltip();
  228. }
  229. }
  230. ImGui::End();
  231. }
  232. void ImGuiTreemapImpl::WeighAndComputeLayout(int w, int h)
  233. {
  234. WeighNodes();
  235. ComputeLayout(w, h);
  236. AssignColors();
  237. m_dirty = false;
  238. }
  239. void ImGuiTreemapImpl::WeighNodes()
  240. {
  241. // The goal of this function is to ensure that every node in the tree starting with m_root has the
  242. // following data initialized:
  243. // - m_parent (pointer to the parent node)
  244. // - m_level (depth of the node (e.g. distance from the root node)
  245. // - m_weight (cumulative some of weights of all children, descending to the leaves)
  246. //
  247. // In addition, this function computes the values in m_levelWeights, which is the sum of the weights
  248. // for all nodes at each depth level in the tree
  249. if (!m_dirty)
  250. {
  251. return;
  252. }
  253. // Flatten the tree via BFS into different levels.
  254. for (auto& nodes : m_levelSets)
  255. {
  256. nodes.clear();
  257. }
  258. CHECK_STACK_EMPTY();
  259. m_stack.emplace_back(&m_root);
  260. m_levelWeights.clear();
  261. while (!m_stack.empty())
  262. {
  263. TreemapNode* node = m_stack.back();
  264. m_stack.pop_back();
  265. if (node->m_children.empty())
  266. {
  267. AZ_Warning(
  268. "Profiler::ImGuiTreemap", node->m_weight >= 0.f,
  269. "Treemap node %s in treemap %s has a negative weight. Only weights >= 0.f are premitted.",
  270. node->m_name.IsEmpty() ? "[unnamed]" : node->m_name.GetCStr(), m_name.GetCStr());
  271. if (node->m_weight < 0.f)
  272. {
  273. // Clamp the node weight below to zero to ensure negative weights don't throw off the algorithm
  274. node->m_weight = 0.f;
  275. }
  276. }
  277. else
  278. {
  279. node->m_weight = 0.f;
  280. if (node->m_tag == 0 || (m_currentMask & node->m_tag) > 0)
  281. {
  282. for (TreemapNode& child : node->m_children)
  283. {
  284. child.m_level = node->m_level + 1;
  285. child.m_parent = node;
  286. m_stack.emplace_back(&child);
  287. }
  288. }
  289. }
  290. if (!node->m_group.IsEmpty())
  291. {
  292. auto it = m_groups.find(node->m_group);
  293. if (it == m_groups.end())
  294. {
  295. GroupConfig& config = m_groups.try_emplace(node->m_group).first->second;
  296. NameToHueSaturation(node->m_group, config.m_hue, config.m_saturation);
  297. }
  298. }
  299. if (m_levelSets.size() < node->m_level + 1)
  300. {
  301. m_levelSets.resize(node->m_level + 1);
  302. }
  303. // Track this node in one of the top level vectors. For non-leaf nodes we'll need to
  304. // accumulate its weight to its parent after all children are accounted for.
  305. m_levelSets[node->m_level].emplace_back(node);
  306. }
  307. // At this point, we've visited every node in the tree and initialized for the weights of all
  308. // non-leaf nodes. Now we have to accumulate values for the intermediate nodes, starting from the
  309. // last level working our way to the front.
  310. // Note that levels[0] is a single node vector containing m_root so we skip this level in our
  311. // traversal (it has no parent).
  312. auto end = m_levelSets.rend() - 1;
  313. for (auto it = m_levelSets.rbegin(); it != end; ++it)
  314. {
  315. for (TreemapNode* node : *it)
  316. {
  317. if (node->m_tag == 0 || (m_currentMask & node->m_tag) > 0)
  318. {
  319. node->m_parent->m_weight += node->m_weight;
  320. }
  321. }
  322. }
  323. // Weights are determined for the root node and every intermediate node, so we can now determine
  324. // the weight sums across all nodes of a given level. Notice that the sums for the leaf nodes have
  325. // already been accumulated at this point.
  326. m_levelWeights.resize(m_levelSets.size());
  327. int level = 0;
  328. for (const auto& nodes : m_levelSets)
  329. {
  330. float& sum = m_levelWeights[level];
  331. for (TreemapNode* node : nodes)
  332. {
  333. if (node->m_tag == 0 || (m_currentMask & node->m_tag) > 0)
  334. {
  335. sum += node->m_weight;
  336. }
  337. }
  338. ++level;
  339. }
  340. }
  341. void ImGuiTreemapImpl::AssignColors()
  342. {
  343. // Here we determine the color for each node, taking into account any selection filters and cursor
  344. // hover state.
  345. if (!m_dirty)
  346. {
  347. return;
  348. }
  349. CHECK_STACK_EMPTY();
  350. for (TreemapNode& node : m_root.m_children)
  351. {
  352. m_stack.push_back(&node);
  353. }
  354. while (!m_stack.empty())
  355. {
  356. TreemapNode& node = *m_stack.back();
  357. m_stack.pop_back();
  358. float hue;
  359. float saturation;
  360. if (node.m_parent == &m_root)
  361. {
  362. // We're looking at one of the user-supplied root nodes. Use the name to determine chromaticity
  363. NameToHueSaturation(node.m_name, hue, saturation);
  364. }
  365. else
  366. {
  367. // We're an intermediate or leaf node, not marked by a highlighted group so simply derive
  368. // chromaticity from the parent node.
  369. hue = node.m_parent->m_hue;
  370. saturation = node.m_parent->m_saturation;
  371. }
  372. node.m_hue = hue;
  373. node.m_saturation = saturation;
  374. // The value in the HSV color is based on the depth of this node in the tree, remapped to the
  375. // [0.4, 0.8] range (subtract 1 from node level to ignore root level).
  376. node.m_value = 0.4f * static_cast<float>(node.m_level - 1) / m_levelWeights.size() + 0.4f;
  377. for (TreemapNode& child : node.m_children)
  378. {
  379. m_stack.push_back(&child);
  380. }
  381. }
  382. }
  383. // This is the function referred to as "worst" in the Squarified paper. The idea is to determine how the
  384. // element aspect ratio changes if an element is added to row. The grade refers to the worst aspect ratio
  385. // among existing elements in the row. The sum, min, and max values correspond to areas within the row.
  386. static double GradeRow(double sum, double min, double max, int extent)
  387. {
  388. // The multiplication and division order here is somewhat haphazard but done this way to improve precision
  389. return AZStd::max(extent / sum * extent / sum * max, sum / extent * sum / extent / min);
  390. }
  391. void ImGuiTreemapImpl::SquarifyChildren(TreemapNode& node)
  392. {
  393. // The paper indicates better layouts were produced when sorting entries in descending weight order
  394. AZStd::vector<TreemapNode*> children;
  395. children.reserve(node.m_children.size() + 1);
  396. for (TreemapNode& child : node.m_children)
  397. {
  398. if (child.m_tag == 0 || (m_currentMask & child.m_tag) > 0)
  399. {
  400. children.push_back(&child);
  401. }
  402. }
  403. // This dummy node at the end is needed to finalize the last row (which will be the last child node
  404. // occupying the row by itself).
  405. TreemapNode endSentinel;
  406. endSentinel.m_weight = 0.f;
  407. children.push_back(&endSentinel);
  408. AZStd::sort(
  409. children.begin(), children.end(),
  410. [](const TreemapNode* lhs, const TreemapNode* rhs)
  411. {
  412. return lhs->m_weight > rhs->m_weight;
  413. });
  414. // Shrink the frame corresponding to a node to ensure there's a 2 pixel gutter.
  415. int rowExtent[2] = { node.m_extent[0] - 4, node.m_extent[1] - 4 };
  416. int rowOffset[2] = { node.m_offset[0] + 2, node.m_offset[1] + 2 };
  417. bool horizontal = rowExtent[1] > rowExtent[0];
  418. // The "extent" here tracks the extent along the rows orientation. The row we lay out entries within could be oriented vertically
  419. // depending on the aspect ratio.
  420. int extent = horizontal ? rowExtent[0] : rowExtent[1];
  421. AZStd::vector<TreemapNode*> row;
  422. double rowArea = 0.f;
  423. double rowMinArea = 0.f;
  424. double rowMaxArea = 0.f;
  425. double grade = 0.f;
  426. // The aspect ratios are computed with respect to element areas, so compute those areas here
  427. double scale = rowExtent[0] * rowExtent[1] / node.m_weight;
  428. for (TreemapNode* child : children)
  429. {
  430. child->m_area = child->m_weight * scale;
  431. }
  432. for (auto it = children.begin(); it != children.end(); /* iterator conditionally incremented in loop body */)
  433. {
  434. TreemapNode& child = **it;
  435. // If the row is empty, unconditionally start a new row.
  436. if (row.empty())
  437. {
  438. row.push_back(&child);
  439. rowArea = child.m_area;
  440. rowMinArea = child.m_area;
  441. rowMaxArea = child.m_area;
  442. grade = GradeRow(rowArea, rowMinArea, rowMaxArea, extent);
  443. ++it;
  444. continue;
  445. }
  446. // Check if this node belongs in the current row, or if we should start a new one
  447. double gradeIfAdded =
  448. GradeRow(rowArea + child.m_area, AZStd::min(child.m_area, rowMinArea), AZStd::max(child.m_area, rowMaxArea), extent);
  449. if (gradeIfAdded < grade)
  450. {
  451. grade = gradeIfAdded;
  452. // Adding this node improves the aspect ratio (nudges it closer to 1, aka makes it more like
  453. // a square) so we should append it to the current row
  454. row.push_back(&child);
  455. rowArea += child.m_area;
  456. if (child.m_area < rowMinArea)
  457. {
  458. rowMinArea = child.m_area;
  459. }
  460. if (child.m_area > rowMaxArea)
  461. {
  462. rowMaxArea = child.m_area;
  463. }
  464. ++it;
  465. continue;
  466. }
  467. // We're starting a new row, which means we need to finalize the layout of the current row.
  468. // Extent in the direction perpendicular to the orientation of the row
  469. int secondaryExtent = static_cast<int>(rowArea / extent);
  470. int offset[2] = { rowOffset[0], rowOffset[1] };
  471. for (TreemapNode* rowNode : row)
  472. {
  473. if (secondaryExtent <= 1)
  474. {
  475. // These nodes are too small to display
  476. rowNode->m_area = 0.0;
  477. }
  478. int nodeExtent = static_cast<int>(rowNode->m_area / secondaryExtent);
  479. rowNode->m_offset[0] = offset[0];
  480. rowNode->m_offset[1] = offset[1];
  481. if (horizontal)
  482. {
  483. rowNode->m_extent[0] = nodeExtent;
  484. rowNode->m_extent[1] = secondaryExtent;
  485. offset[0] += nodeExtent;
  486. }
  487. else
  488. {
  489. rowNode->m_extent[1] = nodeExtent;
  490. rowNode->m_extent[0] = secondaryExtent;
  491. offset[1] += nodeExtent;
  492. }
  493. // Clamp node position within row top left boundary
  494. rowNode->m_offset[0] = AZStd::clamp(rowNode->m_offset[0], rowOffset[0], rowOffset[0] + rowExtent[0]);
  495. rowNode->m_offset[1] = AZStd::clamp(rowNode->m_offset[1], rowOffset[1], rowOffset[1] + rowExtent[1]);
  496. // Clamp node extent based on row bottom right boundary
  497. if (rowNode->m_offset[0] + rowNode->m_extent[0] > rowOffset[0] + rowExtent[0])
  498. {
  499. rowNode->m_extent[0] = rowOffset[0] + rowExtent[0] - rowNode->m_offset[0];
  500. }
  501. if (rowNode->m_offset[1] + rowNode->m_extent[1] > rowOffset[1] + rowExtent[1])
  502. {
  503. rowNode->m_extent[1] = rowOffset[1] + rowExtent[1] - rowNode->m_offset[1];
  504. }
  505. }
  506. if (horizontal)
  507. {
  508. rowExtent[1] -= secondaryExtent;
  509. rowOffset[1] += secondaryExtent;
  510. }
  511. else
  512. {
  513. rowExtent[0] -= secondaryExtent;
  514. rowOffset[0] += secondaryExtent;
  515. }
  516. horizontal = rowExtent[1] > rowExtent[0];
  517. extent = horizontal ? rowExtent[0] : rowExtent[1];
  518. row.clear();
  519. // NOTE: we don't increment the iterator here. Since this node will be used to initialize the next row.
  520. }
  521. }
  522. void ImGuiTreemapImpl::ComputeLayout(int w, int h)
  523. {
  524. if (m_lastExtent[0] == w && m_lastExtent[1] == h)
  525. {
  526. return;
  527. }
  528. // This function implements the "Squarified Treemap" algorithm as documented here:
  529. // https://www.win.tue.nl/~vanwijk/stm.pdf
  530. // (archive link: https://web.archive.org/web/20220224165104/https://www.win.tue.nl/~vanwijk/stm.pdf)
  531. // After function completion, every node will have a computed offset and extent
  532. m_root.m_offset[0] = 0;
  533. m_root.m_offset[1] = 0;
  534. m_root.m_extent[0] = w;
  535. m_root.m_extent[1] = h;
  536. // One modification to the paper implementation is that layout generation is done using a stack
  537. // instead of recursion
  538. CHECK_STACK_EMPTY();
  539. m_stack.push_back(&m_root);
  540. while (!m_stack.empty())
  541. {
  542. TreemapNode* node = m_stack.back();
  543. m_stack.pop_back();
  544. if (!node->m_children.empty())
  545. {
  546. SquarifyChildren(*node);
  547. for (TreemapNode& child : node->m_children)
  548. {
  549. // Leaf nodes don't need to be pushed onto the stack
  550. if (!child.m_children.empty())
  551. {
  552. if (node->m_tag == 0 || (m_currentMask & child.m_tag) > 0)
  553. {
  554. m_stack.push_back(&child);
  555. }
  556. }
  557. }
  558. }
  559. }
  560. m_lastExtent[0] = w;
  561. m_lastExtent[1] = h;
  562. }
  563. } // namespace Profiler