From e575d3fd3d4597c32310d6baec224c298cf751f0 Mon Sep 17 00:00:00 2001 From: Mart G Date: Wed, 6 Jan 2021 19:40:49 +0100 Subject: [PATCH] Utilities: Add a disk space usage analyzation program. SpaceAnalyzer: Partially address code review changes. - Use GUI::CommonActions::make_about_action(). - Pass large arguments by const reference instead of by value. - Mark const functions as such. - Add newline at end of SpaceAnalyzer.af - Use full words instead of abbreviations in variable names. - Use application's namespace instead of 'TreeMap'. - move() certain assignments. - Use member declaration initialization. - Initialize TreeNode* member of QueueEntry. - Rewrite find_mount_for_path to return MountInfo* instead. - Rename ITreeMap and ITreeMapNode to TreeMap and TreeMapNode. - Replace ext suffix with rect suffix for rectangles. SpaceAnalyzer: Further address code review and coding style. - Remove get prefix from accessor functions. - Layout algorithm in its own function, with callback. - Remove nullptr comparisons. - Store lstat errors in error_accumulator. - Use Rect::shatter. - Use Rect's orientation based functions. SpaceAnalyzer: Make sort_children_by_area const qualified. --- Applications/CMakeLists.txt | 1 + Applications/SpaceAnalyzer/CMakeLists.txt | 10 + Applications/SpaceAnalyzer/SpaceAnalyzer.gml | 20 + Applications/SpaceAnalyzer/TreeMapWidget.cpp | 376 +++++++++++++++++++ Applications/SpaceAnalyzer/TreeMapWidget.h | 90 +++++ Applications/SpaceAnalyzer/main.cpp | 300 +++++++++++++++ Base/res/apps/SpaceAnalyzer.af | 4 + Base/res/icons/16x16/app-space-analyzer.png | Bin 0 -> 5564 bytes Base/res/icons/32x32/app-space-analyzer.png | Bin 0 -> 952 bytes 9 files changed, 801 insertions(+) create mode 100644 Applications/SpaceAnalyzer/CMakeLists.txt create mode 100644 Applications/SpaceAnalyzer/SpaceAnalyzer.gml create mode 100644 Applications/SpaceAnalyzer/TreeMapWidget.cpp create mode 100644 Applications/SpaceAnalyzer/TreeMapWidget.h create mode 100644 Applications/SpaceAnalyzer/main.cpp create mode 100644 Base/res/apps/SpaceAnalyzer.af create mode 100644 Base/res/icons/16x16/app-space-analyzer.png create mode 100644 Base/res/icons/32x32/app-space-analyzer.png diff --git a/Applications/CMakeLists.txt b/Applications/CMakeLists.txt index b58ceb84c6..41803202e6 100644 --- a/Applications/CMakeLists.txt +++ b/Applications/CMakeLists.txt @@ -17,6 +17,7 @@ add_subdirectory(Piano) add_subdirectory(PixelPaint) add_subdirectory(QuickShow) add_subdirectory(SoundPlayer) +add_subdirectory(SpaceAnalyzer) add_subdirectory(Spreadsheet) add_subdirectory(SystemMonitor) add_subdirectory(ThemeEditor) diff --git a/Applications/SpaceAnalyzer/CMakeLists.txt b/Applications/SpaceAnalyzer/CMakeLists.txt new file mode 100644 index 0000000000..23cc18f13e --- /dev/null +++ b/Applications/SpaceAnalyzer/CMakeLists.txt @@ -0,0 +1,10 @@ +compile_gml(SpaceAnalyzer.gml SpaceAnalyzerGML.h space_analyzer_gml) + +set(SOURCES + main.cpp + TreeMapWidget.cpp + SpaceAnalyzerGML.h +) + +serenity_app(SpaceAnalyzer ICON app-space-analyzer) +target_link_libraries(SpaceAnalyzer LibGfx LibGUI) diff --git a/Applications/SpaceAnalyzer/SpaceAnalyzer.gml b/Applications/SpaceAnalyzer/SpaceAnalyzer.gml new file mode 100644 index 0000000000..a4a3363c6a --- /dev/null +++ b/Applications/SpaceAnalyzer/SpaceAnalyzer.gml @@ -0,0 +1,20 @@ +@GUI::Widget { + layout: @GUI::VerticalBoxLayout { + spacing: 0 + } + + @GUI::ToolBarContainer { + @GUI::BreadcrumbBar { + fixed_height: 25 + name: "breadcrumb_bar" + } + } + + @SpaceAnalyzer::TreeMapWidget { + name: "tree_map" + } + + @GUI::StatusBar { + name: "status_bar" + } +} diff --git a/Applications/SpaceAnalyzer/TreeMapWidget.cpp b/Applications/SpaceAnalyzer/TreeMapWidget.cpp new file mode 100644 index 0000000000..97d9b58a9e --- /dev/null +++ b/Applications/SpaceAnalyzer/TreeMapWidget.cpp @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2021, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "TreeMapWidget.h" +#include +#include +#include +#include +#include + +namespace SpaceAnalyzer { + +REGISTER_WIDGET(SpaceAnalyzer, TreeMapWidget) + +TreeMapWidget::TreeMapWidget() + : m_viewpoint(0) +{ +} + +TreeMapWidget::~TreeMapWidget() +{ +} + +static const Color colors[] = { + Color(253, 231, 37), + Color(148, 216, 64), + Color(60, 188, 117), + Color(31, 150, 139), + Color(45, 112, 142), + Color(63, 71, 136), + Color(85, 121, 104), +}; + +static float get_normalized_aspect_ratio(float a, float b) +{ + if (a < b) { + return a / b; + } else { + return b / a; + } +} + +static bool node_is_leaf(const TreeMapNode& node) +{ + return node.num_children() == 0; +} + +bool TreeMapWidget::rect_can_contain_label(const Gfx::IntRect& rect) const +{ + return rect.height() > font().presentation_size() && rect.width() > 20; +} + +bool TreeMapWidget::rect_can_contain_children(const Gfx::IntRect& rect) const +{ + return rect.height() > 10 && rect.width() > 10; +} + +Gfx::IntRect TreeMapWidget::inner_rect_for_frame(const Gfx::IntRect& rect) const +{ + const int margin = 5; + Gfx::IntRect tmp_rect = rect; + tmp_rect.shrink(2, 2); // border + tmp_rect.shrink(2, 2); // shading + if (rect_can_contain_label(rect)) { + tmp_rect.set_y(tmp_rect.y() + font().presentation_size() + margin); + tmp_rect.set_height(tmp_rect.height() - (font().presentation_size() + margin * 2)); + tmp_rect.set_x(tmp_rect.x() + margin); + tmp_rect.set_width(tmp_rect.width() - margin * 2); + } + return tmp_rect; +} + +void TreeMapWidget::paint_cell_frame(GUI::Painter& painter, const TreeMapNode& node, const Gfx::IntRect& cell_rect, int depth, bool fill_frame) const +{ + const Gfx::IntRect border_rect = cell_rect.shrunken(2, 2); + const Gfx::IntRect outer_rect = border_rect.shrunken(2, 2); + const Gfx::IntRect inner_rect = inner_rect_for_frame(cell_rect); + + painter.clear_clip_rect(); + painter.add_clip_rect(cell_rect); + Color color = colors[depth % (sizeof(colors) / sizeof(colors[0]))]; + if (m_selected_node_cache == &node) { + color = color.darkened(0.8f); + } + + // Draw borders. + painter.draw_rect(cell_rect, Color::Black, false); + painter.draw_line(border_rect.bottom_left(), border_rect.top_left(), color.lightened()); + painter.draw_line(border_rect.top_left(), border_rect.top_right(), color.lightened()); + painter.draw_line(border_rect.top_right(), border_rect.bottom_right(), color.darkened()); + painter.draw_line(border_rect.bottom_left(), border_rect.bottom_right(), color.darkened()); + + // Paint the background. + if (fill_frame) { + painter.fill_rect(outer_rect, color); + } else { + for (auto& shard : outer_rect.shatter(inner_rect)) { + painter.fill_rect(shard, color); + } + } + + // Paint text. + if (rect_can_contain_label(outer_rect)) { + Gfx::IntRect text_rect = outer_rect; + text_rect.move_by(2, 2); + painter.draw_text(text_rect, node.name(), font(), Gfx::TextAlignment::TopLeft, Color::Black); + if (node_is_leaf(node)) { + text_rect.move_by(0, font().presentation_size() + 1); + painter.draw_text(text_rect, human_readable_size(node.area()), font(), Gfx::TextAlignment::TopLeft, Color::Black); + } + } +} + +template +void TreeMapWidget::lay_out_children(const TreeMapNode& node, const Gfx::IntRect& rect, int depth, Function callback) +{ + if (node.num_children() == 0) { + return; + } + + // Check if the children are sorted yet, if not do that now. + for (size_t k = 0; k < node.num_children() - 1; k++) { + if (node.child_at(k).area() < node.child_at(k + 1).area()) { + node.sort_children_by_area(); + break; + } + } + + int total_area = node.area(); + Gfx::IntRect canvas = rect; + bool remaining_nodes_are_too_small = false; + for (size_t i = 0; !remaining_nodes_are_too_small && i < node.num_children(); i++) { + const int i_node_area = node.child_at(i).area(); + if (i_node_area == 0) + break; + + const int long_side_size = max(canvas.width(), canvas.height()); + const int short_side_size = min(canvas.width(), canvas.height()); + + int row_or_column_size = (long long int)long_side_size * i_node_area / total_area; + int node_area_sum = i_node_area; + size_t k = i + 1; + + // Try to add nodes to this row or column so long as the worst aspect ratio of + // the new set of nodes is better than the worst aspect ratio of the current set. + { + float best_worst_aspect_ratio_so_far = get_normalized_aspect_ratio(row_or_column_size, short_side_size); + for (; k < node.num_children(); k++) { + // Do a preliminary calculation of the worst aspect ratio of the nodes at index i and k + // if that aspect ratio is better than the 'best_worst_aspect_ratio_so_far' we keep it, + // otherwise it is discarded. + int k_node_area = node.child_at(k).area(); + if (k_node_area == 0) { + break; + } + int new_node_area_sum = node_area_sum + k_node_area; + int new_row_or_column_size = (long long int)long_side_size * new_node_area_sum / total_area; + int i_node_size = (long long int)short_side_size * i_node_area / new_node_area_sum; + int k_node_size = (long long int)short_side_size * k_node_area / new_node_area_sum; + float i_node_aspect_ratio = get_normalized_aspect_ratio(new_row_or_column_size, i_node_size); + float k_node_aspect_ratio = get_normalized_aspect_ratio(new_row_or_column_size, k_node_size); + float new_worst_aspect_ratio = min(i_node_aspect_ratio, k_node_aspect_ratio); + if (new_worst_aspect_ratio < best_worst_aspect_ratio_so_far) { + break; + } + best_worst_aspect_ratio_so_far = new_worst_aspect_ratio; + node_area_sum = new_node_area_sum; + row_or_column_size = new_row_or_column_size; + } + } + + // Paint the elements from 'i' up to and including 'k-1'. + { + const int fixed_side_size = row_or_column_size; + int placement_area = node_area_sum; + int main_dim = short_side_size; + + // Lay out nodes in a row or column. + Orientation orientation = canvas.width() > canvas.height() ? Orientation::Horizontal : Orientation::Vertical; + Gfx::IntRect layout_rect = canvas; + layout_rect.set_primary_size_for_orientation(orientation, fixed_side_size); + for (size_t q = i; q < k; q++) { + auto& child = node.child_at(q); + int node_size = (long long int)main_dim * child.area() / placement_area; + Gfx::IntRect cell_rect = layout_rect; + cell_rect.set_secondary_size_for_orientation(orientation, node_size); + Gfx::IntRect inner_rect = inner_rect_for_frame(cell_rect); + bool is_visual_leaf = child.num_children() == 0 || !rect_can_contain_children(inner_rect); + callback(child, q, cell_rect, depth, is_visual_leaf ? IsVisualLeaf::Yes : IsVisualLeaf::No, IsRemainder::No); + if (cell_rect.width() * cell_rect.height() < 16) { + remaining_nodes_are_too_small = true; + } else { + lay_out_children(child, inner_rect, depth + 1, callback); + } + layout_rect.set_secondary_offset_for_orientation(orientation, layout_rect.secondary_offset_for_orientation(orientation) + node_size); + main_dim -= node_size; + placement_area -= child.area(); + } + canvas.set_primary_offset_for_orientation(orientation, canvas.primary_offset_for_orientation(orientation) + fixed_side_size); + canvas.set_primary_size_for_orientation(orientation, canvas.primary_size_for_orientation(orientation) - fixed_side_size); + } + + // Consume nodes that were added to this row or column. + i = k - 1; + total_area -= node_area_sum; + } + + // If not the entire canvas was filled with nodes, fill the remaining area with a dither pattern. + if (!canvas.is_empty()) { + callback(node, 0, canvas, depth, IsVisualLeaf::No, IsRemainder::Yes); + } +} + +const TreeMapNode* TreeMapWidget::path_node(size_t n) const +{ + if (!m_tree.ptr()) + return nullptr; + const TreeMapNode* iter = &m_tree->root(); + size_t path_index = 0; + while (iter && path_index < m_path.size() && path_index < n) { + size_t child_index = m_path[path_index]; + if (child_index >= iter->num_children()) { + return nullptr; + } + iter = &iter->child_at(child_index); + path_index++; + } + return iter; +} + +void TreeMapWidget::paint_event(GUI::PaintEvent& event) +{ + GUI::Frame::paint_event(event); + GUI::Painter painter(*this); + + m_selected_node_cache = path_node(m_path.size()); + + const TreeMapNode* node = path_node(m_viewpoint); + if (!node) { + painter.fill_rect(frame_inner_rect(), Color::MidGray); + } else if (node_is_leaf(*node)) { + paint_cell_frame(painter, *node, frame_inner_rect(), m_viewpoint - 1, true); + } else { + lay_out_children(*node, frame_inner_rect(), m_viewpoint, [&](const TreeMapNode& node, int, const Gfx::IntRect& rect, int depth, IsVisualLeaf visual_leaf, IsRemainder remainder) { + if (remainder == IsRemainder::No) { + bool fill = visual_leaf == IsVisualLeaf::Yes ? true : false; + paint_cell_frame(painter, node, rect, depth, fill); + } else { + Color color = colors[depth % (sizeof(colors) / sizeof(colors[0]))]; + painter.clear_clip_rect(); + painter.add_clip_rect(rect); + painter.draw_rect(rect, Color::Black); + painter.fill_rect_with_dither_pattern(rect.shrunken(2, 2), color, Color::Black); + } + }); + } +} + +Vector TreeMapWidget::path_to_position(const Gfx::IntPoint& position) +{ + const TreeMapNode* node = path_node(m_viewpoint); + if (!node) { + return {}; + } + Vector path; + lay_out_children(*node, frame_inner_rect(), m_viewpoint, [&](const TreeMapNode&, int index, const Gfx::IntRect& rect, int, IsVisualLeaf, IsRemainder is_remainder) { + if (is_remainder == IsRemainder::No && rect.contains(position)) { + path.append(index); + } + }); + return path; +} + +void TreeMapWidget::mousedown_event(GUI::MouseEvent& event) +{ + const TreeMapNode* node = path_node(m_viewpoint); + if (node && !node_is_leaf(*node)) { + Vector path = path_to_position(event.position()); + if (!path.is_empty()) { + m_path.shrink(m_viewpoint); + m_path.append(path); + if (on_path_change) { + on_path_change(); + } + update(); + } + } +} + +void TreeMapWidget::doubleclick_event(GUI::MouseEvent& event) +{ + const TreeMapNode* node = path_node(m_viewpoint); + if (node && !node_is_leaf(*node)) { + Vector path = path_to_position(event.position()); + m_path.shrink(m_viewpoint); + m_path.append(path); + m_viewpoint = m_path.size(); + if (on_path_change) { + on_path_change(); + } + update(); + } +} + +void TreeMapWidget::mousewheel_event(GUI::MouseEvent& event) +{ + int delta = event.wheel_delta(); + // FIXME: The wheel_delta is premultiplied in the window server, we actually want a raw value here. + int step_size = GUI::WindowServerConnection::the().send_sync()->step_size(); + if (delta > 0) { + size_t step_back = delta / step_size; + if (step_back > m_viewpoint) + step_back = m_viewpoint; + set_viewpoint(m_viewpoint - step_back); + } else { + size_t step_up = (-delta) / step_size; + set_viewpoint(m_viewpoint + step_up); + } +} + +void TreeMapWidget::set_tree(RefPtr tree) +{ + m_tree = tree; + m_path.clear(); + m_viewpoint = 0; + if (on_path_change) { + on_path_change(); + } + update(); +} + +void TreeMapWidget::set_viewpoint(size_t viewpoint) +{ + if (viewpoint > m_path.size()) + viewpoint = m_path.size(); + m_viewpoint = viewpoint; + if (on_path_change) { + on_path_change(); + } + update(); +} + +size_t TreeMapWidget::path_size() const +{ + return m_path.size() + 1; +} + +size_t TreeMapWidget::viewpoint() const +{ + return m_viewpoint; +} + +} diff --git a/Applications/SpaceAnalyzer/TreeMapWidget.h b/Applications/SpaceAnalyzer/TreeMapWidget.h new file mode 100644 index 0000000000..8b4e1e9269 --- /dev/null +++ b/Applications/SpaceAnalyzer/TreeMapWidget.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2021, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include + +namespace SpaceAnalyzer { + +struct TreeMapNode { + virtual String name() const = 0; + virtual int64_t area() const = 0; + virtual size_t num_children() const = 0; + virtual const TreeMapNode& child_at(size_t i) const = 0; + virtual void sort_children_by_area() const = 0; +}; + +struct TreeMap : public RefCounted { + virtual ~TreeMap() { } + virtual const TreeMapNode& root() const = 0; +}; + +class TreeMapWidget final : public GUI::Frame { + C_OBJECT(TreeMapWidget) + +public: + virtual ~TreeMapWidget() override; + Function on_path_change; + size_t path_size() const; + const TreeMapNode* path_node(size_t n) const; + size_t viewpoint() const; + void set_viewpoint(size_t); + void set_tree(RefPtr tree); + +private: + TreeMapWidget(); + virtual void paint_event(GUI::PaintEvent&) override; + virtual void mousedown_event(GUI::MouseEvent&) override; + virtual void doubleclick_event(GUI::MouseEvent&) override; + virtual void mousewheel_event(GUI::MouseEvent&) override; + + bool rect_can_contain_children(const Gfx::IntRect& rect) const; + bool rect_can_contain_label(const Gfx::IntRect& rect) const; + Gfx::IntRect inner_rect_for_frame(const Gfx::IntRect& rect) const; + + enum class IsVisualLeaf { + Yes, + No + }; + enum class IsRemainder { + Yes, + No + }; + + template + void lay_out_children(const TreeMapNode&, const Gfx::IntRect&, int depth, Function); + void paint_cell_frame(GUI::Painter&, const TreeMapNode&, const Gfx::IntRect&, int depth, bool fill) const; + Vector path_to_position(const Gfx::IntPoint&); + + RefPtr m_tree; + Vector m_path; + size_t m_viewpoint; + const void* m_selected_node_cache; +}; + +} diff --git a/Applications/SpaceAnalyzer/main.cpp b/Applications/SpaceAnalyzer/main.cpp new file mode 100644 index 0000000000..b06f92c20d --- /dev/null +++ b/Applications/SpaceAnalyzer/main.cpp @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2021, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#include "TreeMapWidget.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char* APP_NAME = "SpaceAnalyzer"; + +struct TreeNode : public SpaceAnalyzer::TreeMapNode { + TreeNode(String name) + : m_name(move(name)) {}; + + virtual String name() const { return m_name; } + virtual int64_t area() const { return m_area; } + virtual size_t num_children() const + { + if (m_children) { + return m_children->size(); + } + return 0; + } + virtual const TreeNode& child_at(size_t i) const { return m_children->at(i); } + virtual void sort_children_by_area() const + { + if (m_children) { + Vector* children = const_cast*>(m_children.ptr()); + quick_sort(*children, [](auto& a, auto& b) { return b.m_area < a.m_area; }); + } + } + + String m_name; + int64_t m_area { 0 }; + OwnPtr> m_children; +}; + +struct Tree : public SpaceAnalyzer::TreeMap { + Tree(String root_name) + : m_root(move(root_name)) {}; + virtual ~Tree() {}; + TreeNode m_root; + virtual const SpaceAnalyzer::TreeMapNode& root() const override + { + return m_root; + }; +}; + +struct MountInfo { + String mount_point; + String source; +}; + +static void fill_mounts(Vector& output) +{ + // Output info about currently mounted filesystems. + auto df = Core::File::construct("/proc/df"); + if (!df->open(Core::IODevice::ReadOnly)) { + fprintf(stderr, "Failed to open /proc/df: %s\n", df->error_string()); + return; + } + + auto content = df->read_all(); + auto json = JsonValue::from_string(content); + ASSERT(json.has_value()); + + json.value().as_array().for_each([&output](auto& value) { + auto filesystem_object = value.as_object(); + MountInfo mount_info; + mount_info.mount_point = filesystem_object.get("mount_point").to_string(); + mount_info.source = filesystem_object.get("source").as_string_or("none"); + output.append(mount_info); + }); +} + +static MountInfo* find_mount_for_path(String path, Vector& mounts) +{ + MountInfo* result = nullptr; + size_t length = 0; + for (auto& mount_info : mounts) { + String& mount_point = mount_info.mount_point; + if (path.starts_with(mount_point)) { + if (!result || mount_point.length() > length) { + result = &mount_info; + length = mount_point.length(); + } + } + } + return result; +} + +static long long int update_totals(TreeNode& node) +{ + long long int result = 0; + if (node.m_children) { + for (auto& child : *node.m_children) { + result += update_totals(child); + } + node.m_area = result; + } else { + result = node.m_area; + } + return result; +} + +struct QueueEntry { + QueueEntry(String path, TreeNode* node) + : path(move(path)) + , node(node) {}; + String path; + TreeNode* node { nullptr }; +}; + +static void populate_filesize_tree(TreeNode& root, Vector& mounts, HashMap& error_accumulator) +{ + ASSERT(!root.m_name.ends_with("/")); + + Queue queue; + queue.enqueue(QueueEntry(root.m_name, &root)); + + StringBuilder builder = StringBuilder(); + builder.append(root.m_name); + builder.append("/"); + MountInfo* root_mount_info = find_mount_for_path(builder.to_string(), mounts); + if (!root_mount_info) { + return; + } + while (!queue.is_empty()) { + QueueEntry queue_entry = queue.dequeue(); + + builder.clear(); + builder.append(queue_entry.path); + builder.append("/"); + + MountInfo* mount_info = find_mount_for_path(builder.to_string(), mounts); + if (!mount_info || (mount_info != root_mount_info && mount_info->source != root_mount_info->source)) { + continue; + } + + Core::DirIterator dir_iterator(builder.to_string(), Core::DirIterator::SkipParentAndBaseDir); + if (dir_iterator.has_error()) { + int error_sum = error_accumulator.get(dir_iterator.error()).value_or(0); + error_accumulator.set(dir_iterator.error(), error_sum + 1); + } else { + queue_entry.node->m_children = make>(); + while (dir_iterator.has_next()) { + queue_entry.node->m_children->append(TreeNode(dir_iterator.next_path())); + } + for (auto& child : *queue_entry.node->m_children) { + String& name = child.m_name; + int name_len = name.length(); + builder.append(name); + struct stat st; + int stat_result = lstat(builder.to_string().characters(), &st); + if (stat_result < 0) { + int error_sum = error_accumulator.get(errno).value_or(0); + error_accumulator.set(errno, error_sum + 1); + } else { + if (S_ISDIR(st.st_mode)) { + queue.enqueue(QueueEntry(builder.to_string(), &child)); + } else { + child.m_area = st.st_size; + } + } + builder.trim(name_len); + } + } + } + + update_totals(root); +} + +static void analyze(RefPtr tree, SpaceAnalyzer::TreeMapWidget& treemapwidget, GUI::StatusBar& statusbar) +{ + // Build an in-memory tree mirroring the filesystem and for each node + // calculate the sum of the file size for all its descendants. + TreeNode* root = &tree->m_root; + Vector mounts; + fill_mounts(mounts); + HashMap error_accumulator; + populate_filesize_tree(*root, mounts, error_accumulator); + + // Display an error summary in the statusbar. + if (!error_accumulator.is_empty()) { + StringBuilder builder; + bool first = true; + builder.append("Some directories were not analyzed: "); + for (auto& key : error_accumulator.keys()) { + if (!first) { + builder.append(", "); + } + builder.append(strerror(key)); + builder.append(" ("); + int value = error_accumulator.get(key).value(); + builder.append(String::number(value)); + if (value == 1) { + builder.append(" time"); + } else { + builder.append(" times"); + } + builder.append(")"); + first = false; + } + statusbar.set_text(builder.to_string()); + } else { + statusbar.set_text("No errors"); + } + treemapwidget.set_tree(tree); +} + +int main(int argc, char* argv[]) +{ + auto app = GUI::Application::construct(argc, argv); + + RefPtr tree = adopt(*new Tree("")); + + // Configure application window. + auto app_icon = GUI::Icon::default_icon("app-space-analyzer"); + auto window = GUI::Window::construct(); + window->set_title(APP_NAME); + window->resize(640, 480); + window->set_icon(app_icon.bitmap_for_size(16)); + + // Load widgets. + auto& mainwidget = window->set_main_widget(); + mainwidget.load_from_gml(space_analyzer_gml); + auto& breadcrumbbar = *mainwidget.find_descendant_of_type_named("breadcrumb_bar"); + auto& treemapwidget = *mainwidget.find_descendant_of_type_named("tree_map"); + auto& statusbar = *mainwidget.find_descendant_of_type_named("status_bar"); + + // Configure the menubar. + auto menubar = GUI::MenuBar::construct(); + auto& app_menu = menubar->add_menu(APP_NAME); + app_menu.add_action(GUI::Action::create("Analyze", [&](auto&) { + analyze(tree, treemapwidget, statusbar); + })); + app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { + app->quit(); + })); + auto& help_menu = menubar->add_menu("Help"); + help_menu.add_action(GUI::CommonActions::make_about_action(APP_NAME, app_icon, window)); + app->set_menubar(move(menubar)); + + // Configure event handlers. + breadcrumbbar.on_segment_click = [&](size_t index) { + ASSERT(index < treemapwidget.path_size()); + treemapwidget.set_viewpoint(index); + }; + treemapwidget.on_path_change = [&]() { + breadcrumbbar.clear_segments(); + for (size_t k = 0; k < treemapwidget.path_size(); k++) { + if (k == 0) { + breadcrumbbar.append_segment("/"); + } else { + const SpaceAnalyzer::TreeMapNode* node = treemapwidget.path_node(k); + breadcrumbbar.append_segment(node->name()); + } + } + breadcrumbbar.set_selected_segment(treemapwidget.viewpoint()); + }; + + // At startup automatically do an analysis of root. + analyze(tree, treemapwidget, statusbar); + + window->show(); + return app->exec(); +} diff --git a/Base/res/apps/SpaceAnalyzer.af b/Base/res/apps/SpaceAnalyzer.af new file mode 100644 index 0000000000..387837d7d0 --- /dev/null +++ b/Base/res/apps/SpaceAnalyzer.af @@ -0,0 +1,4 @@ +[App] +Name=SpaceAnalyzer +Executable=/bin/SpaceAnalyzer +Category=Utilities diff --git a/Base/res/icons/16x16/app-space-analyzer.png b/Base/res/icons/16x16/app-space-analyzer.png new file mode 100644 index 0000000000000000000000000000000000000000..eba583d8f464c48c5de78850a877b6e64e675a31 GIT binary patch literal 5564 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!tocT|N$lmsP~D-;yvr)B1( zDwI?fq$;FVWTr7NRNT5cEqc;t4W8EfTe~8egv>X}tP9=T9QjXX!`#f}lkNSM#~+*0 zSRlw=%hdkMzMlUVcmD?7CpA|lI2lyL6nPqHWo=VmKc_eP=fA@;Q-iJVTCwq1F+Z?}X7K@+lm$tL8`u=CfJn&8aMi zPkqYO>9@;eijwGpwAr^bJXe{rnB}R-2|2EbWZlfQM{I4;aF+ z`MBr!wI1c&4$(YcG`4R(w(%|71En=@729+>9QRU z-6aZNcqrdrVVE1Sf9dNhKNhHUUy+f>zhYo|>@dsu*GK%*6x8}0ZTy%xGG?CX*;MQ# zd^aWS(w?(>9G1>6kn&GbcwyBoUz7Y@ey6Z`(L3e5q6v#T-bgPI(^ximgAl zt~_>0$)x{b$*PSOb($GsXZ_CTe2Cp0a_MSgIL}SF&spA4;oe`mMbo=KkNZst>>My8UJ0y=z5RH5+b*ZT+(Du>R*S1$*UBES|iN|H(zq`|O`y z?EHOiviteF-{;xv^7y_KLj3alU^~g(o^xOO6+1iW-8R2|*}ZO{V%4o%;ne3 zdz>57z2i;X@9h0O-k}e!^Ygz}?4P&(`#c+~m%*Q3mw&tcZqejHp0O8|xD})1SvHUlh*@ znzSK?fq}6#)7d$|)7cr8hZz_u=G0EK^*9_L(&`_)R7sT8y2HX{g`b##^S z3*t)WyiY#9az4Ys^aoz~zfPMb{F?kFE4W*Z@$T&dM@xf)zcrkeUGV}1OnC3`zAn+mIn+=ATHl0=1y+?>2(s|s5s zu(?)w#a19;eI*63l9Fs&r3l{u1?T*tR0R_~6Fmc6*NV(CBPBa71)HLjG^-#NH>eRs zDQUJ!86_nJR{Hwo<>h+i#(Mch>H3D2mX`VkM*2oZx~^eb{9HY4kC_w)^b z>j4F0dS-3`SO-WovdWZXxVnPUq8zZAlw|$XoYdUZypm#lLp?(j50zx5AtDIHKOh-! z5Fi_0ky`*aA4N4RyugaV;cDfQpIi#E)YHXQ3FKa@l>Fq(6e}>(&?qr6DJ9WZH_bHF zNY^CA)KWJo)gnpP+}PC6%sAD|(9$Rw$tcgf;*!L?G_$lYv@|odFf%lPI1OxGSZYymW_}*XOhW@b zBO|c(lw>Qn{G!~%5?iIr+{E-${erx7ummVtto(~IQ}ap^L3zVg$q+1Fky~KpT$Gwv zl3x^(pPyr^1TtH}NYBs!oTU_OK$*j}qQuH4KN+kMtT;8r4xFFBxz;H$T_4P~(Z{D4 z!=&Jh{PH}oMo7-ZV-7+o+!V*6lJfkbZ2uzvq^#8B68z?1Qw%pLC^bE^xTL7klYpnt z)Y(9it4C&WNq$i!lKsJ{g%BRZTREBFpi)o(7Xns^$&lbLPAp4J0Y`xXOe#4eF)uwe z#a0QL)nMY8Sdy`UMM|nkvXOCp?v8Ap_lA%R%l8LFQk$Ebr>BaeJ zCFO}lsgCKXc_p?=?wPp-;CNQh0H;1pRMq7fsd=_a+6IPJ1_nxy=vA=M2NkF=@7aKg z9A956Wup(S{6G~UBrHG` z5iKz^xzW-J1%**dNDAN4;2I4sl0twa#iOZfG`L6#0g@DtrY@=l7Z;+3otl?ot5mLJ zZ+HHiMjA5L~c#`D6wL2F?PH$YKTtZeb8+WSBKaf`Ng7y~NYkmHjCr zvyiyJA~{!O1_p*$PZ!4!i_^&o3j_?DUjBdbD!p-I&bKK^C*%rWrp+ii%((iTOfNGJ z2>48?O1#3r#wPYeVEGA#yG?dHCr=A^b(k}VHyky;$@ZsqU03Lw$WPxCI$67ng_rUr aFfcT)6XeN0wSf;bKH%x<=d#Wzp$Pz2_A+b$ literal 0 HcmV?d00001 diff --git a/Base/res/icons/32x32/app-space-analyzer.png b/Base/res/icons/32x32/app-space-analyzer.png new file mode 100644 index 0000000000000000000000000000000000000000..1b5654c6d26d3515f8ba7b3fdab3ada99b419840 GIT binary patch literal 952 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANNft(nfw0iMpz3I#>^X_+~x z3=A3*YbV-z91aj^^$%XEB+6>tVd1jEPt3(tb}N_b3W2QFFj=3tU&l*7KsP*B*V%Egc^ZJ+7Toe*WR!-wX@Gl4e~sy z+H+j%x$e2Bm4~FhKhfuT{^hCay30}yOxzJIEP`BGHyk+4lD52F65(;N^3L)5?|-Ql zX+F8dpnTls)CGmzbsy}1*hXvH+dWfleeCeN`*95q7Z)&1+=?>$|2!0lS>jAV&Y=QUhg%0i1b?ksoG zf4Q-_JbqXGuRHr6e5{b4!?8qA$VFjMX8r7D;l^yKj^cve1EzeJIF6irXZ`x;nQO~_ z3m^G7)qalhmZ$OpWoax7YH6K9ht5qCn0Lg`!J^v6PU5p)b7gI-U;}r`?3`UQE!WNP zWtwNWomEsadd8%kA~L4)r_cMIzgtuAgZN1!)fFdxHYIN=G1Lw2Wng#{Bcl+kmvG~q zXX*Zn2mk(-TXT)!s8`v-ytq0Iv4y&c4`voIGE6zpJ#~$VRuEI-gTrO~3{wuQy{2H* z=*x0VjpuZ;N?4Q zh8^)5mnUW`@Yu}sVrIyDU-aYWnbub)SVEKwf>`C|9V*Qc3}CchsdXpB?XJMGRIcBJ zQr@3`^6NS6UBVtWyO1;fYO>0g{S2S;PlqPuKl%See0J=<>u*n4e`H`_VDNPHb6Mw< G&;$T>E}n`2 literal 0 HcmV?d00001