콘텐츠로 건너뛰기
Codex

번들 크기를 실측하는 방법

번들 크기를 실측하는 방법

TECH NOTE · DESKTOP AI PET

앱이 무겁다는 감을 숫자로 바꾸는 방법입니다. ls와 du가 다른 값을 주는 이유, 심볼릭 링크와 하드링크를 잘못 세면 생기는 일을 실제 측정 결과와 함께 정리했습니다.

"무겁다"는 감이고 9.8M은 숫자다

이 시리즈는 208M짜리 앱을 보고 엔진을 갈아탄 이야기로 시작했다. 그 판단의 근거가 숫자였다. 그 글에서는 결과를 적었고, 이번 편은 그 숫자를 어떻게 재는지다.

앱을 만들고 배포하는 쪽의 마지막 편이다.

https://github.com/bluecafe/pet-reference/tree/main/size-measure

경로를 주면 무엇이 얼마나 차지하는지 표로 나온다.

재보기 전에는 모른다

이 앱의 에셋 폴더를 재봤다.

  파일        3
  논리 합계          9.9M

확장자별
  png               9.8M   99.7%  1개
  json             32.0K    0.3%  2개

큰 파일
       9.8M  sprite-sheet-alpha.png
      24.0K  manifest.json
       8.0K  rules.json

스프라이트 시트 한 장이 99.7%다.

이 숫자가 나오면 할 일이 정해진다. 매니페스트를 아무리 줄여도 소용없다. 시트의 해상도를 낮추거나, 안 쓰는 프레임을 빼거나, 압축 방식을 바꾸는 것만 의미가 있다.

반대로 재보지 않으면 JSON 구조를 다듬거나 파일 이름을 줄이는 데 시간을 쓰게 된다. 줄일 곳을 감으로 고르면 대개 틀린다.

`ls`와 `du`가 다른 값을 준다

여기부터가 재는 일의 함정이다. 같은 폴더를 두 도구로 재면 값이 다르게 나온다.

파일 시스템은 블록 단위로 자리를 준다. 1바이트짜리 파일도 블록 하나를 통째로 차지한다. 그래서 작은 파일이 많으면 논리 크기 합계보다 실제 사용량이 훨씬 크다.

두 값을 이렇게 나눠 읽는다.

report.entries.push(Entry {
    path: path.to_path_buf(),
    logical: metadata.len(),          // ls 가 보여주는 값
    on_disk: disk_usage(&metadata),   // du 가 보여주는 값
});

fn disk_usage(metadata: &std::fs::Metadata) -> u64 {
    // blocks()는 512바이트 단위다. 파일 시스템의 블록 크기와 다르다.
    metadata.blocks() * 512
}

blocks()가 돌려주는 단위가 함정이다. 파일 시스템의 블록 크기(보통 4K)가 아니라 고정 512바이트다. 파일 시스템 블록 크기를 곱하면 여덟 배 큰 값이 나온다.

예제 테스트에 1바이트 파일 열 개를 넣어 뒀다. 논리 합계는 10바이트인데 디스크 사용량은 그보다 훨씬 크게 나온다.

어느 쪽이 맞는가는 무엇을 알고 싶은지에 따라 갈린다.

알고 싶은 것 봐야 하는 값
배포 파일이 얼마나 커질까 논리 크기
사용자 디스크를 얼마나 먹을까 디스크 사용량

그래서 둘 다 보여준다. 하나만 보여주면 나머지 상황에서 틀린 답을 준다.

심볼릭 링크를 따라가면 두 번 센다

.app 번들 안을 열어 보면 링크가 많다. 특히 Frameworks 아래에는 버전 폴더를 가리키는 링크가 여러 개 있다.

재귀로 훑으면서 링크를 따라가면 같은 파일을 몇 번씩 센다. 링크가 번들 밖을 가리키면 밖의 크기까지 합쳐진다. 앱이 실제보다 훨씬 커 보이는 결과가 나오고, 그 숫자를 믿고 엉뚱한 것을 줄이려 든다.

그래서 링크는 그 자체로만 보고 따라가지 않는다. 예제 테스트가 이걸 확인한다 — 16K 파일 하나와 그것을 가리키는 링크가 있을 때 결과는 16K다. 따라갔다면 32K가 나온다.

하드링크는 실체가 하나다

심볼릭 링크보다 알아채기 어려운 게 하드링크다. 겉보기에는 그냥 파일이라 링크인지 구분이 안 된다.

여러 경로에서 보이지만 디스크에는 한 번만 있다. 경로마다 세면 실제보다 큰 값이 나온다.

파일마다 (기기 번호, inode 번호) 쌍을 기억해 두고, 이미 본 것이면 건너뛴다.

if metadata.nlink() > 1 && !seen.insert((metadata.dev(), metadata.ino())) {
    report.skipped_hardlinks += 1;
    return Ok(());
}

nlink() > 1을 먼저 보는 것이 중요하다. 링크가 하나뿐인 파일은 중복될 수 없으므로 집합에 넣을 필요도 없다. 파일이 수천 개인 번들에서 대부분이 여기서 걸러진다.

inode만으로는 부족하고 기기 번호가 함께 필요하다. 다른 파일 시스템에 있는 두 파일이 같은 inode 번호를 가질 수 있어서, 그걸 같은 파일로 보면 실제보다 작은 값이 나온다.

이건 재는 도구를 직접 만들지 않으면 마주치지 않는 문제다. du는 이미 이렇게 하고 있다.

확장자별로 묶으면 답이 한 줄로 나온다

파일 목록을 훑는 것보다 확장자별 합계가 빠르다. “png가 99.7%”는 한 줄로 무엇을 줄여야 하는지 말해준다.

pub fn by_extension(&self) -> Vec<(String, u64, usize)> {
    let mut totals: HashMap<String, (u64, usize)> = HashMap::new();
    for entry in &self.entries {
        let key = entry
            .path
            .extension()
            .map(|ext| ext.to_string_lossy().to_lowercase())
            .unwrap_or_else(|| "(없음)".to_string());
        let slot = totals.entry(key).or_insert((0, 0));
        slot.0 += entry.on_disk;
        slot.1 += 1;
    }
    ...
}

확장자가 없는 파일을 "(없음)"으로 묶는 것이 그냥 건너뛰는 것보다 낫다. .app 번들의 실행 파일에는 확장자가 없는데, 그게 목록에서 사라지면 합계가 안 맞는다.

소문자로 바꾸는 것도 필요하다. .PNG.png가 따로 세어지면 비율이 반씩 갈린다.

큰 파일 목록은 상위 10개만 보여준다. 전체 목록은 사람이 못 읽는다. 파일이 수천 개인 번들에서 전부 찍으면 스크롤만 하다 끝난다.

pub fn largest(&self, n: usize) -> Vec<&Entry> {
    let mut sorted: Vec<&Entry> = self.entries.iter().collect();
    sorted.sort_by(|a, b| b.on_disk.cmp(&a.on_disk).then(a.path.cmp(&b.path)));
    sorted.into_iter().take(n).collect()
}

크기가 같을 때 경로로 한 번 더 정렬한다. 이게 없으면 같은 폴더를 두 번 재도 순서가 달라져서, 결과를 비교할 수 없다.

숫자도 소수점 한 자리까지만 쓴다. 9.8M9.83M은 판단을 바꾸지 않는다.

여기서 멈춘 곳

정직하게 적으면 이 도구가 답하지 못하는 것이 둘 있다.

압축 후 크기. 배포하는 것은 ZIP이다. 텍스트는 크게 줄고 PNG는 거의 안 준다. 그래서 압축 전 비율과 압축 후 비율이 다르다. 정확히 알려면 실제로 압축해 봐야 한다.

바이너리 내부. 실행 파일 안에서 무엇이 자리를 차지하는지는 심볼 테이블을 읽어야 한다. 이 예제는 파일 단위까지만 본다.

둘 다 넣을 수는 있었지만 넣지 않았다. 파일 단위 측정만으로 대부분의 결정이 난다. 이 앱도 그랬다 — 어느 파일이 큰지만 알면 충분했다.

정리

크기를 줄이려면 먼저 재야 한다. 재는 일이 단순해 보이지만 세 군데서 갈린다.

  • 논리 크기와 디스크 사용량은 다르다. 무엇을 알고 싶은지에 따라 골라야 한다
  • 심볼릭 링크를 따라가면 같은 것을 여러 번 세고, 번들 밖까지 합친다
  • 하드링크는 실체가 하나다. inode로 걸러야 한다
  • 확장자별 합계가 파일 목록보다 빨리 답을 준다
  • 상위 몇 개만 보여준다. 전체 목록은 읽히지 않는다

돌아가는 코드는 pet-reference/size-measure에 있다. 아무 폴더에나 대고 돌려볼 수 있다.

을 받아서 압축을 풀고 이 도구를 대 보면, 지금까지 다섯 편에서 만든 것들이 실제로 얼마씩 차지하는지 볼 수 있다.

여기까지가 창을 띄우고 그림을 붙여 배포하는 쪽이다. 다음 편부터는 성격이 다른 코드로 넘어간다. 캐릭터가 말하고 듣고 기억하는 쪽이다. 첫 순서는 음성 인식인데, 아무도 말하지 않았는데 자막이 들어오는 문제부터 다룬다.

이 블로그 더 보기

이 블로그는 실제 프로젝트를 AI와 함께 굴리면서 남긴 기록입니다.

  • 새 글은 RSS로 받아볼 수 있습니다.
  • 시리즈 전체는 여기에 정리해 두었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다