chromium/ios/chrome/app/application_storage_metrics.mm

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/app/application_storage_metrics.h"

#import <Foundation/Foundation.h>

#import "base/apple/foundation_util.h"
#import "base/base_paths.h"
#import "base/files/file_enumerator.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/metrics/histogram_macros.h"
#import "base/path_service.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/thread_pool.h"
#import "components/history/core/browser/history_constants.h"
#import "components/optimization_guide/core/optimization_guide_constants.h"
#import "ios/chrome/browser/shared/model/paths/paths.h"

// The etension used for all snapshot images.
constexpr std::string_view kSnapshotImageExtension = ".jpg";
// The label appended to the snapshot filename for grey snapshot images.
constexpr std::string_view kGreySnapshotImageIdentifier = "Grey";

// The path, relative to the profile directory, where tab state is stored.
const base::FilePath::CharType kSessionsPath[] = FILE_PATH_LITERAL("Sessions");

// The path, relative to the profile directory, where snapshots are stored.
const base::FilePath::CharType kSnapshotsPath[] =
    FILE_PATH_LITERAL("Snapshots");

// The path, relative to the application's Library directory, to WebKit's
// storage location for website local data .
const base::FilePath::CharType kWebsiteLocalDataPath[] =
    FILE_PATH_LITERAL("WebKit/WebsiteData/Default");

// The path, relative to the tmp directory, used for WebKit tmp data.
const base::FilePath::CharType kWebKitTmpPath[] = FILE_PATH_LITERAL("WebKit");

// The path, relative to the Caches directory, used for WebKit cache.
const base::FilePath::CharType kWebKitCachePath[] = FILE_PATH_LITERAL("WebKit");

struct DirectorySnapshotDetails {
  DirectorySnapshotDetails() : snapshot_count(0), total_size_bytes(0) {}

  int snapshot_count;
  int64_t total_size_bytes;
};

DirectorySnapshotDetails CalculateImageMetricsInRoot(bool grey_only,
                                                     base::FilePath root) {
  DirectorySnapshotDetails details;

  if (!base::PathExists(root)) {
    return details;
  }

  base::File file(root, base::File::FLAG_OPEN | base::File::FLAG_READ);
  base::File::Info info;
  if (!file.IsValid() || !file.GetInfo(&info)) {
    return details;
  }

  if (!info.is_directory) {
    if (root.MatchesExtension(kSnapshotImageExtension)) {
      // Add this snapshot to the total if search for all snapshots or searching
      // for grey snapshots only and the filename contains
      // `kGreySnapshotImageIdentifier`.
      if (!grey_only ||
          root.BaseName().MaybeAsASCII().find(kGreySnapshotImageIdentifier) !=
              std::string::npos) {
        details.snapshot_count += 1;
        details.total_size_bytes += info.size;
      }
    }
    return details;
  }

  base::FileEnumerator enumerator(
      root, /*recursive=*/false,
      base::FileEnumerator::DIRECTORIES | base::FileEnumerator::FILES);
  for (base::FilePath path = enumerator.Next(); !path.empty();
       path = enumerator.Next()) {
    DirectorySnapshotDetails child_details =
        CalculateImageMetricsInRoot(grey_only, root.Append(path.BaseName()));
    details.snapshot_count += child_details.snapshot_count;
    details.total_size_bytes += child_details.total_size_bytes;
  }

  return details;
}
// Calculates and returns the total size used by `root`.
int64_t CalculateTotalSize(base::FilePath root) {
  if (!base::PathExists(root)) {
    return 0;
  }

  base::File file(root, base::File::FLAG_OPEN | base::File::FLAG_READ);
  base::File::Info info;
  if (!file.IsValid() || !file.GetInfo(&info)) {
    return 0;
  }

  if (!info.is_directory) {
    return info.size;
  }

  int64_t total_directory_size = 0;

  base::FileEnumerator enumerator(
      root, /*recursive=*/false,
      base::FileEnumerator::DIRECTORIES | base::FileEnumerator::FILES);
  for (base::FilePath path = enumerator.Next(); !path.empty();
       path = enumerator.Next()) {
    int64_t dir_item_size = CalculateTotalSize(root.Append(path.BaseName()));
    total_directory_size += dir_item_size;
  }
  return total_directory_size;
}

// Returns the path to the sandbox "Application Support" directory.
base::FilePath GetApplicationSupportDirectory() {
  base::FilePath application_support_path;
  base::PathService::Get(base::DIR_APP_DATA, &application_support_path);
  return application_support_path;
}

// Returns the path to the sandbox "Caches" directory.
base::FilePath GetCachesDirectory() {
  NSArray* cachesDirectories = NSSearchPathForDirectoriesInDomains(
      NSCachesDirectory, NSUserDomainMask, YES);
  NSString* cachePath = [cachesDirectories objectAtIndex:0];
  return base::apple::NSStringToFilePath(cachePath);
}

// Logs the WebKit and Chrome Cache directory sizes. Accepts a task runner as a
// parameter in order to keep it in scope throughout the execution.
void LogCacheDirectorySizes(scoped_refptr<base::SequencedTaskRunner>) {
  base::FilePath caches_path = GetCachesDirectory();

  base::FilePath webkit_caches_path = caches_path.Append(kWebKitCachePath);
  int64_t webkit_cache_size_bytes = CalculateTotalSize(webkit_caches_path);
  UMA_HISTOGRAM_MEMORY_MEDIUM_MB("IOS.SandboxMetrics.WebKitCacheSize",
                                 webkit_cache_size_bytes / 1024 / 1024);

  int64_t total_cache_size_bytes = CalculateTotalSize(caches_path);
  int64_t chrome_cache_size_bytes =
      total_cache_size_bytes - webkit_cache_size_bytes;
  UMA_HISTOGRAM_MEMORY_MEDIUM_MB("IOS.SandboxMetrics.ChromeCacheSize",
                                 chrome_cache_size_bytes / 1024 / 1024);
}

// Logs the "Documents" directory size. Accepts a task runner as a parameter in
// order to keep it in scope throughout the execution.
void LogDocumentsDirectorySize(scoped_refptr<base::SequencedTaskRunner>) {
  base::FilePath documents_path = base::apple::GetUserDocumentPath();
  int total_size_bytes = CalculateTotalSize(documents_path);
  UMA_HISTOGRAM_MEMORY_MEDIUM_MB("IOS.SandboxMetrics.DocumentsSize2",
                                 total_size_bytes / 1024 / 1024);
}

// Logs the Favicons storage size. Accepts a task runner as a parameter in
// order to keep it in scope throughout the execution.
void LogFaviconsStorageSize(base::FilePath profile_path,
                            scoped_refptr<base::SequencedTaskRunner>) {
  base::FilePath favicons_path =
      profile_path.Append(history::kFaviconsFilename);
  int64_t total_size_bytes = CalculateTotalSize(favicons_path);
  UMA_HISTOGRAM_MEMORY_KB("IOS.SandboxMetrics.FaviconsSize",
                          total_size_bytes / 1024);
}

// Logs the "Library" directory size. Accepts a task runner as a parameter in
// order to keep it in scope throughout the execution.
void LogLibraryDirectorySize(scoped_refptr<base::SequencedTaskRunner>) {
  base::FilePath library_path = base::apple::GetUserLibraryPath();
  int total_size_bytes = CalculateTotalSize(library_path);
  UMA_HISTOGRAM_MEMORY_MEDIUM_MB("IOS.SandboxMetrics.LibrarySize",
                                 total_size_bytes / 1024 / 1024);
}

// Logs the optimization guide model downloads directory size. Accepts a task
// runner as a parameter in order to keep it in scope throughout the execution.
void LogOptimizationGuideModelDownloadsMetrics(
    scoped_refptr<base::SequencedTaskRunner>) {
  int items = 0;

  base::FilePath models_dir =
      base::PathService::CheckedGet(ios::DIR_USER_DATA)
          .Append(optimization_guide::kOptimizationGuideModelStoreDirPrefix);
  if (base::PathExists(models_dir)) {
    int total_size_bytes = CalculateTotalSize(models_dir);
    UMA_HISTOGRAM_MEMORY_MEDIUM_MB(
        "IOS.SandboxMetrics.OptimizationGuideModelDownloadsSize",
        total_size_bytes / 1024 / 1024);

    base::FileEnumerator enumerator(models_dir, /*recursive=*/false,
                                    base::FileEnumerator::DIRECTORIES);
    for (base::FilePath path = enumerator.Next(); !path.empty();
         path = enumerator.Next()) {
      items++;
    }
  }

  UMA_HISTOGRAM_COUNTS_1000(
      "IOS.SandboxMetrics.OptimizationGuideModelDownloadedItems", items);
}

// Logs the total amount of storage used by the regular and OTR tabs for both
// tab state and snapshots and the total size of the Application Support
// directory excluding the storage used by tabs.
void LogApplicationSupportDirectorySize(
    base::FilePath profile_path,
    base::FilePath otr_profile_path,
    scoped_refptr<base::SequencedTaskRunner>) {
  base::FilePath session_storage_dir = profile_path.Append(kSessionsPath);
  int64_t regular_tabs_size_bytes = CalculateTotalSize(session_storage_dir);
  UMA_HISTOGRAM_MEMORY_MEDIUM_MB("IOS.SandboxMetrics.TotalRegularSessionSize",
                                 regular_tabs_size_bytes / 1024 / 1024);

  base::FilePath otr_storage_dir = otr_profile_path.Append(kSessionsPath);
  int64_t otr_tabs_size_bytes = CalculateTotalSize(otr_storage_dir);
  UMA_HISTOGRAM_MEMORY_MEDIUM_MB("IOS.SandboxMetrics.TotalOTRSessionSize",
                                 otr_tabs_size_bytes / 1024 / 1024);

  int64_t tab_storage_size = regular_tabs_size_bytes + otr_tabs_size_bytes;
  int64_t application_support_size =
      CalculateTotalSize(GetApplicationSupportDirectory());
  int64_t size_without_tabs_data = application_support_size - tab_storage_size;
  UMA_HISTOGRAM_MEMORY_MEDIUM_MB("IOS.SandboxMetrics.ApplicationSupportSize",
                                 size_without_tabs_data / 1024 / 1024);
}

void LogAverageSnapshotSizes(base::FilePath profile_path,
                             scoped_refptr<base::SequencedTaskRunner>) {
  base::FilePath snapshots_storage_dir = profile_path.Append(kSnapshotsPath);

  DirectorySnapshotDetails grey_snapshot_details =
      CalculateImageMetricsInRoot(/*grey_only=*/true, snapshots_storage_dir);
  int64_t grey_average_bytes_size = 0;
  if (grey_snapshot_details.snapshot_count > 0) {
    grey_average_bytes_size = grey_snapshot_details.total_size_bytes /
                              grey_snapshot_details.snapshot_count;
  }
  UMA_HISTOGRAM_MEMORY_KB("IOS.SandboxMetrics.AverageGreySnapshotSize",
                          grey_average_bytes_size / 1024);

  DirectorySnapshotDetails all_snapshot_details =
      CalculateImageMetricsInRoot(/*grey_only=*/false, snapshots_storage_dir);
  int64_t color_total_size_bytes = all_snapshot_details.total_size_bytes -
                                   grey_snapshot_details.total_size_bytes;
  int color_snapshot_count = all_snapshot_details.snapshot_count -
                             grey_snapshot_details.snapshot_count;
  int64_t color_average_bytes_size = 0;
  if (color_snapshot_count > 0) {
    color_average_bytes_size = color_total_size_bytes / color_snapshot_count;
  }
  UMA_HISTOGRAM_MEMORY_KB("IOS.SandboxMetrics.AverageColorSnapshotSize",
                          color_average_bytes_size / 1024);
}

// Logs the WebKit tmp directory size and the size of the tmp directory
// excluding the WebKit directory. Accepts a task runner as a parameter in order
// to keep it in scope throughout the execution.
void LogTmpDirectorySizes(base::FilePath profile_path,
                          scoped_refptr<base::SequencedTaskRunner>) {
  base::FilePath tmp_dir;
  if (GetTempDir(&tmp_dir)) {
    base::FilePath tmp_webkit_path = profile_path.Append(kWebKitTmpPath);
    int webkit_tmp_size_bytes = CalculateTotalSize(tmp_webkit_path);
    UMA_HISTOGRAM_MEMORY_MEDIUM_MB("IOS.SandboxMetrics.WebKitTempSize",
                                   webkit_tmp_size_bytes / 1024 / 1024);

    int total_tmp_size_bytes = CalculateTotalSize(tmp_dir);
    int tmp_without_webkit_data = total_tmp_size_bytes - webkit_tmp_size_bytes;
    UMA_HISTOGRAM_MEMORY_MEDIUM_MB("IOS.SandboxMetrics.ChromeTempSize",
                                   tmp_without_webkit_data / 1024 / 1024);
  }
}

// Logs the `kWebsiteLocalDataPath` directory size. Accepts a task runner as a
// parameter in order to keep it in scope throughout the execution.
void LogWebsiteLocalDataSize(base::FilePath profile_path,
                             scoped_refptr<base::SequencedTaskRunner>) {
  base::FilePath library_path = base::apple::GetUserLibraryPath();
  base::FilePath website_data_dir = profile_path.Append(kWebsiteLocalDataPath);
  int total_size_bytes = CalculateTotalSize(website_data_dir);
  UMA_HISTOGRAM_MEMORY_MEDIUM_MB("IOS.SandboxMetrics.WebsiteLocalData",
                                 total_size_bytes / 1024 / 1024);
}

void LogApplicationStorageMetrics(base::FilePath profile_path,
                                  base::FilePath off_the_record_state_path) {
  scoped_refptr<base::SequencedTaskRunner> task_runner =
      base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
           base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN});

  task_runner->PostTask(
      FROM_HERE,
      base::BindOnce(&LogApplicationSupportDirectorySize, profile_path,
                     off_the_record_state_path, task_runner));
  task_runner->PostTask(FROM_HERE, base::BindOnce(&LogAverageSnapshotSizes,
                                                  profile_path, task_runner));
  task_runner->PostTask(FROM_HERE,
                        base::BindOnce(&LogCacheDirectorySizes, task_runner));
  task_runner->PostTask(
      FROM_HERE, base::BindOnce(&LogDocumentsDirectorySize, task_runner));
  task_runner->PostTask(FROM_HERE, base::BindOnce(&LogFaviconsStorageSize,
                                                  profile_path, task_runner));
  task_runner->PostTask(FROM_HERE,
                        base::BindOnce(&LogLibraryDirectorySize, task_runner));
  task_runner->PostTask(
      FROM_HERE,
      base::BindOnce(&LogOptimizationGuideModelDownloadsMetrics, task_runner));
  task_runner->PostTask(FROM_HERE, base::BindOnce(&LogTmpDirectorySizes,
                                                  profile_path, task_runner));
  task_runner->PostTask(FROM_HERE, base::BindOnce(&LogWebsiteLocalDataSize,
                                                  profile_path, task_runner));
}