Перейти к содержанию

Internals

Архитектура

Общая схема

путь
  │
  ▼
openat / getattrlistbulk ──► TreeBuilder ──► ScanTree ──► SwiftUI
  │                            (арена)        │
  │                                           ├─ карта (treemap)
  └─ readdir + fstatat (fallback)              ├─ таблицы
                                               ├─ типы
                                               └─ крупнейшие / очистка

Сканер

Сканирование начинается с проверки, поддерживает ли файловая система getattrlistbulk. Для каждого каталога:

  1. openat(parentFD, name, O_DIRECTORY) — дескриптор не хранится для всех каталогов сразу, поэтому глубина рекурсии ограничена только глубиной пути.
  2. getattrlistbulk с буфером 256 КБ запрашивает: ATTR_CMN_RETURNED_ATTRS, ATTR_CMN_NAME, ATTR_CMN_OBJTYPE, ATTR_CMN_CRTIME, ATTR_CMN_MODTIME, ATTR_DIR_MOUNTSTATUS, ATTR_DIR_ALLOCSIZE, ATTR_FILE_ALLOCSIZE.
  3. Точки монтирования (DIR_MNTSTATUS_MNTPOINT/TRIGGER) пропускаются: приложение не уходит на другие тома и не считает один и тот же APFS-контейнер дважды.
  4. Симлинки не разыменовываются и получают нулевой размер.
  5. Каталоги с расширениями .app, .photoslibrary, .fcpbundle и подобными помечаются как пакеты: они сканируются внутри для точного размера, но в рейтингах и очистке участвуют как один объект.

Для файловых систем без getattrlistbulk есть запасной путь: readdir + fstatat(AT_SYMLINK_NOFOLLOW). Он медленнее, но сохраняет ту же модель дерева.

Прогресс отправляется не чаще 10 раз в секунду; отмена проверяется между пачками записей и каталогами. Ошибки доступа не прерывают скан: путь запоминается в выборке, а счётчик пропущенных объектов показывается в баннере.

Дерево

ScanNode — компактная структура: смещение и длина имени в общем UTF-8-блобе, родитель, диапазон детей, количество файлов, размер, тип, доминирующая категория и две даты. Имена не дублируются строками, поэтому миллионы узлов занимают десятки мегабайт.

  • Файлы меньше 64 КБ агрегируются в узел aggregate внутри своей папки: «Мелкие файлы». Суммы остаются точными, а память не растёт линейно от количества мелочи.
  • Даты создания и изменения приходят из того же getattrlistbulk и хранятся как Int32-дни от 2001-01-01: 8 байт на узел вместо 16 при секундах. Точность — сутки, для UI дата возвращается на полдень UTC, чтобы не смещаться при форматировании в локальном часовом поясе.
  • После обхода один обратный проход суммирует размеры и количество файлов от листьев к корню. Второй проход вычисляет доминирующую категорию контейнера по самому крупному ребёнку — она используется для цвета на карте.
  • Удаление не мутирует существующее дерево. removingSubtrees(at:) создаёт копию (copy-on-write) и вырезает поддеревья, поэтому фоновый расчёт статистики не гоняется с изменениями. GUI сразу получает новое дерево. ScanTreeRemoval.Group запоминает корень и все впервые удалённые узлы, а restoringSubtrees(groups:) возвращает их и суммы — это основа ⌘Z.

Правила очистки

CleanupCatalog — 15 декларативных правил, а CleanupAdvisor сопоставляет их с готовым деревом и возвращает CleanupSuggestion с риском, размером и объяснением. Правила не обращаются к диску: им доступны только ScanTree, домашний путь, текущая дата и набор запущенных приложений. Поэтому каждое правило тестируется на синтетическом дереве, а список можно считать в фоне, не блокируя интерфейс. Вложенные предложения схлопываются, чтобы одна и та же папка не попадала в список дважды.

Подробности и таблица правил — в Правилах очистки.

Коллекция

AppModel.collection — множество Int32 верхнеуровневых узлов текущего дерева. При добавлении вложенные объекты удаляются, поэтому сумма не удваивается. Удаление коллекции идёт через те же removingSubtrees и restoringSubtrees, что и обычная Корзина: подтверждение, отмена ⌘Z и обновление сумм происходят одинаково. Коллекция сбрасывается при новом скане, потому что индексы узлов относятся к конкретному дереву.

Глобальный поиск

ScanSearchIndex лениво строится по первому ⌘F: один проход по дереву складывает имена в нижнем регистре без диакритики, пропуская агрегаты и содержимое пакетов. Поиск идёт по этому массиву в фоне, фильтры размера, типа, категории и даты применяются к узлам дерева. Запрос с / дополнительно сравнивается с относительным путём. Индекс живёт до следующего скана; при смене дерева он пересоздаётся.

Инспектор и QuickLook

Инспектор берёт имя, путь, занятый размер и даты из ScanTree, а логический размер — точечно из URLResourceValues при выборе. Это не требует менять сканер и не добавляет памяти на миллионы узлов. QuickLook открывается модально через quickLookPreview по URL выбранного файла; папки и пакеты просматриваются в Finder.

Локализация

Строки интерфейса — русские ключи в SwiftUI LocalizedStringKey; английские переводы лежат в Sources/DiskScopeKit/Resources/en.lproj/Localizable.strings и копируются в DiskScope.app/Contents/Resources. Настройка языка задаёт environment(\.locale), а L10n и Plural используют её для модельных строк, единиц и длительности. Английский бандл также лежит в SwiftPM resource bundle, поэтому тесты проверяют переводы без запуска приложения.

Похожие изображения

SimilarImageFinder отбирает изображения не меньше порога, пропуская пакеты, агрегаты, симлинки и hard links. Для каждого файла VNGenerateImageFeaturePrintRequest строит вектор признаков; SimilarImageClusterer жадно относит кадр к первому кластеру, расстояние до представителя которого меньше порога. Расстояние — евклидово по векторам; вычисления идут в фоне, на Mac ничего не отправляется. Это эвристика: «похожие» не значит идентичные.

Дубликаты

DuplicateFinder сначала собирает файлы не меньше порога, пропуская пакеты, агрегаты, симлинки и помеченные hard links. Кандидаты группируются по размеру, затем по SHA-256 первых 64 КБ и, для совпавших, по полному SHA-256. Хеширование идёт в фоне; результат содержит только индексы и пути, содержимое не сохраняется. Смена дерева отменяет токен и сбрасывает группы.

Жёсткие ссылки

getattrlistbulk возвращает ATTR_CMN_FILEID после дат в общем блоке атрибутов; fallback использует st_ino. Сессия скана хранит множество fileid и при повторной ссылке записывает размер 0, а исходный размер прибавляет к ScanReport.hardLinkBytes. Флаг ссылки упакован в старший бит ScanNode.kind, поэтому структура узла не выросла. Это делает суммы точными, но не позволяет отличить физический файл от числа directory entries: счётчик файлов по-прежнему считает имена.

Полная картина диска

VolumeInfo хранит volumeAvailableCapacityForImportantUsage; разница с обычным свободным местом даёт оценку purgeable. DiskOverviewService в фоне сканирует ~/.Trash тем же DiskScanner и читает локальные снимки через tmutil listlocalsnapshots. Парсер снимков вынесен в отдельную функцию и покрыт тестами; при ошибке tmutil панель честно показывает предупреждение, а не пустой список. Приложение только читает эти данные.

История сканов

ScanHistoryEntry хранит дату, корень, общий размер, число файлов и размеры папок верхнего уровня. ScanHistoryStore пишет JSON в Application Support и ограничивает историю 30 записями. ScanHistoryDiff сопоставляет запись с текущим ScanTree по именам верхнего уровня и считает дельты. Сравнение разрешено только для одинакового корневого пути; данные не покидают Mac.

Карта

Раскладка — squarified treemap (Bruls, Huizing, van Wijk). Строятся два уровня:

  1. Дети текущей папки, отсортированные по размеру; крупнейшие 320 — отдельные ячейки, остальные — «Ещё N».
  2. Для контейнеров площадью больше порога дети раскладываются внутри с отступом и полосой 18 pt сверху. На полосе — имя родителя и размер. Вложенность ограничена 32 детьми и общим бюджетом в 700 подячеек.

Цвет ячейки берётся из категории (видео, изображения, аудио, архивы, документы, приложения, код, другое). Для контейнеров без выраженной категории используется вращающаяся холодная палитра, чтобы карта из одних .app не превращалась в однотонное пятно. Для светлых категорий текст автоматически становится тёмным.

Потоки и состояние

  • Скан выполняется на отдельной последовательной очереди; UI получает прогресс через DispatchQueue.main.
  • ScanTree неизменяем после скана (кроме copy-on-write удаления), поэтому фоновые вычисления «Крупнейших» и «Типов» читают его без блокировок.
  • Результаты фоновых вычислений применяются только если дерево всё ещё то же самое (проверка идентичности и поколения).
  • Таблица текущей папки показывает не больше 2500 самых крупных детей; для больших каталогов это защищает интерфейс от десятков тысяч строк.

Точность

Сумма скана сравнивается с du -sk в тестах; на реальных каталогах (/Applications, Xcode) значения совпадают. Отличия возможны из-за:

  • жёстких ссылок — они считаются по каждому пути, а du дедуплицирует;
  • клонов APFS — учитываются как обычные файлы;
  • снимков APFS и purgeable-пространства — они не входят в дерево.

Подробнее — в разделе Ограничения.

Производительность

  • /Applications (53,1 ГБ, 698 004 файла): несколько секунд на тёплом кэше.
  • Домашняя папка (238 ГБ, 3,1 млн файлов): около 4 минут на холодном кэше, с прогрессом и возможностью отмены.
  • Память: около 1,06 млн узлов для домашней папки; плоские массивы и агрегация мелких файлов держат footprint в десятках-сотнях мегабайт.