Skip to content

Commit f55ae0e

Browse files
committed
feat: add ll-cli analyze size subcommand
Add a new `analyze size` subcommand to ll-cli that reports on-disk usage of installed layers, breaking each module's footprint into: - exclusive: bytes unique to that module - shared: bytes shared with at least one other module - logical: exclusive + shared (apparent size to the module) - actual: shared bytes divided across the sharing modules Sizes are computed from st_blocks (real block usage, not file size), and hard-linked inodes are deduplicated by (st_dev, st_ino) so they are counted once per sharing module. A separate pass reports the total real disk usage of the repository directory for cross-reference. Options: --sort {actual|logical|exclusive|shared|id} (default: actual) --asc (default: descending) Signed-off-by: reddevillg <reddevillg@gmail.com>
1 parent b229e17 commit f55ae0e

9 files changed

Lines changed: 448 additions & 5 deletions

File tree

apps/ll-cli/src/main.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,40 @@ ll-cli list --upgradable
432432
"application(s), base(s) or runtime(s)"));
433433
}
434434

435+
// Function to add the analyze size subcommand
436+
void addAnalyzeSizeCommand(CLI::App &cliAnalyze, SizeOptions &sizeOptions)
437+
{
438+
auto *cliSize =
439+
cliAnalyze.add_subcommand("size", _("Show installed module sizes and repository real size"))
440+
->fallthrough();
441+
cliSize->usage(_(R"(Usage: ll-cli analyze size [OPTIONS]
442+
443+
Example:
444+
# show installed module sizes
445+
ll-cli analyze size
446+
)"));
447+
cliSize
448+
->add_option(
449+
"--sort",
450+
sizeOptions.sortBy,
451+
_(R"(Sort result by specify field. One of "actual", "logical", "exclusive", "shared" or "id")"))
452+
->type_name("FIELD")
453+
->capture_default_str()
454+
->check(CLI::IsMember({ "actual", "logical", "exclusive", "shared", "id" }));
455+
cliSize->add_flag("--asc", sizeOptions.ascending, _("Sort in ascending order"));
456+
}
457+
458+
// Function to add the analyze subcommands
459+
void addAnalyzeCommand(CLI::App &commandParser, SizeOptions &sizeOptions, const std::string &group)
460+
{
461+
auto *cliAnalyze = commandParser.add_subcommand("analyze", _("Analyze installed applications"))
462+
->group(group)
463+
->usage(_("Usage: ll-cli analyze SUBCOMMAND [OPTIONS]"));
464+
cliAnalyze->require_subcommand(1);
465+
466+
addAnalyzeSizeCommand(*cliAnalyze, sizeOptions);
467+
}
468+
435469
// Function to add the info subcommand
436470
void addInfoCommand(CLI::App &commandParser, InfoOptions &infoOptions, const std::string &group)
437471
{
@@ -566,6 +600,7 @@ You can report bugs to the linyaps team under this project: https://github.com/O
566600
SearchOptions searchOptions{};
567601
UninstallOptions uninstallOptions{};
568602
ListOptions listOptions{};
603+
SizeOptions sizeOptions{};
569604
InfoOptions infoOptions{};
570605
ContentOptions contentOptions{};
571606
linglong::common::cli::RepoOptions repoOptions{};
@@ -587,6 +622,7 @@ You can report bugs to the linyaps team under this project: https://github.com/O
587622
addUpgradeCommand(commandParser, upgradeOptions, CliBuildInGroup);
588623
addSearchCommand(commandParser, searchOptions, CliSearchGroup);
589624
addListCommand(commandParser, listOptions, CliBuildInGroup);
625+
addAnalyzeCommand(commandParser, sizeOptions, CliBuildInGroup);
590626
linglong::common::cli::addRepoCommand(commandParser,
591627
repoOptions,
592628
CliRepoGroup,
@@ -743,6 +779,14 @@ You can report bugs to the linyaps team under this project: https://github.com/O
743779
result = cli->uninstall(uninstallOptions);
744780
} else if (name == "list") {
745781
result = cli->list(listOptions);
782+
} else if (name == "analyze") {
783+
const auto &subcommands = (*ret)->get_subcommands();
784+
auto subcommand = std::find_if(subcommands.begin(), subcommands.end(), [](CLI::App *app) {
785+
return app->parsed();
786+
});
787+
if (subcommand != subcommands.end() && (*subcommand)->get_name() == "size") {
788+
result = cli->size(sizeOptions);
789+
}
746790
} else if (name == "info") {
747791
result = cli->info(infoOptions);
748792
} else if (name == "content") {

libs/linglong/src/linglong/cli/cli.cpp

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,15 @@
6868
#include <string>
6969
#include <system_error>
7070
#include <thread>
71+
#include <unordered_map>
72+
#include <unordered_set>
7173
#include <utility>
7274
#include <vector>
7375

7476
#include <fcntl.h>
7577
#include <pwd.h>
7678
#include <sys/mman.h>
79+
#include <sys/stat.h>
7780
#include <unistd.h>
7881

7982
using namespace linglong::utils::error;
@@ -283,6 +286,169 @@ bool delegateToContainerInit(const std::string &containerID,
283286
return result == 0;
284287
}
285288

289+
struct ModuleSize
290+
{
291+
std::uint64_t exclusiveSize{ 0 };
292+
std::uint64_t sharedSize{ 0 };
293+
std::uint64_t logicalSize{ 0 };
294+
std::uint64_t actualSize{ 0 };
295+
};
296+
297+
struct InodeUsage
298+
{
299+
std::uint64_t diskUsage{ 0 };
300+
std::unordered_set<std::size_t> modules;
301+
};
302+
303+
struct ModuleSizeCalculation
304+
{
305+
std::vector<ModuleSize> moduleSizes;
306+
std::uint64_t actualTotalSize{ 0 };
307+
};
308+
309+
Result<ModuleSizeCalculation>
310+
calculateModuleSizes(const std::vector<std::filesystem::path> &moduleDirs) noexcept
311+
{
312+
LINGLONG_TRACE("calculate module sizes");
313+
314+
std::unordered_map<std::string, InodeUsage> inodeUsages;
315+
std::error_code ec;
316+
317+
auto addEntry = [&](const std::filesystem::path &path,
318+
std::size_t moduleIndex) -> Result<void> {
319+
struct stat64 st{};
320+
if (::lstat64(path.c_str(), &st) == -1) {
321+
const auto err = errno;
322+
return LINGLONG_ERR(fmt::format("failed to stat {}: {}",
323+
path,
324+
linglong::common::error::errorString(err)));
325+
}
326+
327+
const auto diskUsage = static_cast<std::uint64_t>(st.st_blocks) * 512;
328+
const auto inodeKey = std::to_string(st.st_dev) + ":" + std::to_string(st.st_ino);
329+
330+
auto &usage = inodeUsages[inodeKey];
331+
usage.diskUsage = diskUsage;
332+
usage.modules.insert(moduleIndex);
333+
334+
return LINGLONG_OK;
335+
};
336+
337+
for (std::size_t moduleIndex = 0; moduleIndex < moduleDirs.size(); ++moduleIndex) {
338+
const auto &dir = moduleDirs[moduleIndex];
339+
auto addRoot = addEntry(dir, moduleIndex);
340+
if (!addRoot) {
341+
return LINGLONG_ERR(addRoot);
342+
}
343+
344+
auto iterator = std::filesystem::recursive_directory_iterator{
345+
dir,
346+
std::filesystem::directory_options::skip_permission_denied,
347+
ec
348+
};
349+
if (ec) {
350+
return LINGLONG_ERR(fmt::format("failed to open module directory {}", dir), ec);
351+
}
352+
353+
const auto end = std::filesystem::recursive_directory_iterator{};
354+
while (iterator != end) {
355+
const auto &entry = *iterator;
356+
auto addResult = addEntry(entry.path(), moduleIndex);
357+
if (!addResult) {
358+
return LINGLONG_ERR(addResult);
359+
}
360+
361+
iterator.increment(ec);
362+
if (ec) {
363+
return LINGLONG_ERR(fmt::format("failed to iterate module directory {}", dir), ec);
364+
}
365+
}
366+
}
367+
368+
ModuleSizeCalculation calculation;
369+
calculation.moduleSizes.resize(moduleDirs.size());
370+
for (const auto &[_, usage] : inodeUsages) {
371+
const auto moduleCount = usage.modules.size();
372+
if (moduleCount == 0) {
373+
continue;
374+
}
375+
376+
calculation.actualTotalSize += usage.diskUsage;
377+
const auto actualSize = usage.diskUsage / moduleCount;
378+
for (auto moduleIndex : usage.modules) {
379+
auto &moduleSize = calculation.moduleSizes[moduleIndex];
380+
moduleSize.logicalSize += usage.diskUsage;
381+
if (moduleCount == 1) {
382+
moduleSize.exclusiveSize += usage.diskUsage;
383+
moduleSize.actualSize += usage.diskUsage;
384+
} else {
385+
moduleSize.sharedSize += usage.diskUsage;
386+
moduleSize.actualSize += actualSize;
387+
}
388+
}
389+
}
390+
391+
return calculation;
392+
}
393+
394+
Result<std::uint64_t> calculateRealDiskUsage(const std::filesystem::path &dir) noexcept
395+
{
396+
LINGLONG_TRACE("calculate real disk usage");
397+
398+
std::uint64_t size{ 0 };
399+
std::unordered_set<std::string> visitedInodes;
400+
401+
auto addPath = [&](const std::filesystem::path &path) -> Result<void> {
402+
struct stat64 st{};
403+
404+
if (::lstat64(path.c_str(), &st) == -1) {
405+
const auto err = errno;
406+
return LINGLONG_ERR(fmt::format("failed to stat {}: {}",
407+
path,
408+
linglong::common::error::errorString(err)));
409+
}
410+
411+
const auto inodeKey = std::to_string(st.st_dev) + ":" + std::to_string(st.st_ino);
412+
if (st.st_nlink > 1 && !visitedInodes.insert(inodeKey).second) {
413+
return LINGLONG_OK;
414+
}
415+
416+
size += static_cast<std::uint64_t>(st.st_blocks) * 512;
417+
return LINGLONG_OK;
418+
};
419+
420+
auto rootResult = addPath(dir);
421+
if (!rootResult) {
422+
return LINGLONG_ERR(rootResult);
423+
}
424+
425+
std::error_code ec;
426+
auto iterator = std::filesystem::recursive_directory_iterator{
427+
dir,
428+
std::filesystem::directory_options::skip_permission_denied,
429+
ec
430+
};
431+
if (ec) {
432+
return LINGLONG_ERR(fmt::format("failed to open repository directory {}", dir), ec);
433+
}
434+
435+
const auto end = std::filesystem::recursive_directory_iterator{};
436+
while (iterator != end) {
437+
const auto &entry = *iterator;
438+
auto result = addPath(entry.path());
439+
if (!result) {
440+
return LINGLONG_ERR(result);
441+
}
442+
443+
iterator.increment(ec);
444+
if (ec) {
445+
return LINGLONG_ERR(fmt::format("failed to iterate repository directory {}", dir), ec);
446+
}
447+
}
448+
449+
return size;
450+
}
451+
286452
} // namespace
287453

288454
namespace linglong::cli {
@@ -1679,6 +1845,107 @@ int Cli::list(const ListOptions &options)
16791845
return 0;
16801846
}
16811847

1848+
int Cli::size(const SizeOptions &options)
1849+
{
1850+
auto repo = this->getRepo();
1851+
if (!repo) {
1852+
this->printer.printErr(repo.error());
1853+
return -1;
1854+
}
1855+
1856+
auto items = (*repo)->listLayerItem();
1857+
if (!items) {
1858+
this->printer.printErr(items.error());
1859+
return -1;
1860+
}
1861+
1862+
std::vector<Printer::ModuleSizeInfo> moduleSizes;
1863+
moduleSizes.reserve(items->size());
1864+
std::vector<std::filesystem::path> modulePaths;
1865+
modulePaths.reserve(items->size());
1866+
1867+
for (const auto &item : *items) {
1868+
if (item.deleted.value_or(false)) {
1869+
continue;
1870+
}
1871+
1872+
auto ref = package::Reference::fromPackageInfo(item.info);
1873+
if (!ref) {
1874+
this->printer.printErr(ref.error());
1875+
return -1;
1876+
}
1877+
1878+
auto layerDir = (*repo)->getLayerDir(*ref, item.info.packageInfoV2Module);
1879+
if (!layerDir) {
1880+
this->printer.printErr(layerDir.error());
1881+
return -1;
1882+
}
1883+
1884+
moduleSizes.push_back(Printer::ModuleSizeInfo{
1885+
.id = item.info.id,
1886+
.name = item.info.name,
1887+
.version = item.info.version,
1888+
.channel = item.info.channel,
1889+
.module = item.info.packageInfoV2Module,
1890+
});
1891+
modulePaths.push_back(layerDir->path());
1892+
}
1893+
1894+
auto calculatedSizes = calculateModuleSizes(modulePaths);
1895+
if (!calculatedSizes) {
1896+
this->printer.printErr(calculatedSizes.error());
1897+
return -1;
1898+
}
1899+
1900+
for (std::size_t index = 0; index < moduleSizes.size(); ++index) {
1901+
moduleSizes[index].exclusiveSize = calculatedSizes->moduleSizes.at(index).exclusiveSize;
1902+
moduleSizes[index].sharedSize = calculatedSizes->moduleSizes.at(index).sharedSize;
1903+
moduleSizes[index].logicalSize = calculatedSizes->moduleSizes.at(index).logicalSize;
1904+
moduleSizes[index].actualSize = calculatedSizes->moduleSizes.at(index).actualSize;
1905+
}
1906+
1907+
const auto tieByName = [](const auto &info) {
1908+
return std::tie(info.id, info.channel, info.module, info.version);
1909+
};
1910+
std::sort(moduleSizes.begin(),
1911+
moduleSizes.end(),
1912+
[&options, &tieByName](const auto &lhs, const auto &rhs) {
1913+
auto compareSize = [&options, &tieByName, &lhs, &rhs](std::uint64_t lhsSize,
1914+
std::uint64_t rhsSize) {
1915+
if (lhsSize == rhsSize) {
1916+
return tieByName(lhs) < tieByName(rhs);
1917+
}
1918+
1919+
return options.ascending ? lhsSize < rhsSize : lhsSize > rhsSize;
1920+
};
1921+
1922+
if (options.sortBy == "id") {
1923+
return options.ascending ? tieByName(lhs) < tieByName(rhs)
1924+
: tieByName(rhs) < tieByName(lhs);
1925+
}
1926+
if (options.sortBy == "logical") {
1927+
return compareSize(lhs.logicalSize, rhs.logicalSize);
1928+
}
1929+
if (options.sortBy == "exclusive") {
1930+
return compareSize(lhs.exclusiveSize, rhs.exclusiveSize);
1931+
}
1932+
if (options.sortBy == "shared") {
1933+
return compareSize(lhs.sharedSize, rhs.sharedSize);
1934+
}
1935+
1936+
return compareSize(lhs.actualSize, rhs.actualSize);
1937+
});
1938+
1939+
auto repoSize = calculateRealDiskUsage((*repo)->getRepoDir());
1940+
if (!repoSize) {
1941+
this->printer.printErr(repoSize.error());
1942+
return -1;
1943+
}
1944+
1945+
this->printer.printModuleSizes(moduleSizes, calculatedSizes->actualTotalSize, *repoSize);
1946+
return 0;
1947+
}
1948+
16821949
utils::error::Result<std::vector<api::types::v1::UpgradeListResult>> Cli::listUpgradable()
16831950
{
16841951
LINGLONG_TRACE("list upgradable");

libs/linglong/src/linglong/cli/cli.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ struct ListOptions
117117
bool showUpgradeList{ false };
118118
};
119119

120+
struct SizeOptions
121+
{
122+
std::string sortBy{ "actual" };
123+
bool ascending{ false };
124+
};
125+
120126
struct InfoOptions
121127
{
122128
std::string appid;
@@ -181,6 +187,7 @@ class Cli : public QObject
181187
int search(const SearchOptions &options);
182188
int uninstall(const UninstallOptions &options);
183189
int list(const ListOptions &options);
190+
int size(const SizeOptions &options);
184191
int repo(CLI::App *subcommand, const common::cli::RepoOptions &options);
185192
int info(const InfoOptions &options);
186193
int content(const ContentOptions &options);

0 commit comments

Comments
 (0)