ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • font-range가 빠르게 서브셋하는 방법
    프로그래밍/Web 2024. 12. 31. 21:15

    본 글은 2023년 12월 30일에 작성되었으며 옮겨진 글입니다.

    백업해두었다가 귀찮던 나머지 계속 미루다 공개합니다.

     

    font-range란?

    저는 fontRange라는 라이브러리의 저자이며, 폰트 서브셋을 효율적이고 쉽게 할 수 있는 API를 제공합니다.

     

    폰트 파일에서 필요한 문자 집합만을 선택하여 추출한 폰트 파일을 만드는 작업입니다.
    일부 문자들만을 가지고 있으니 파일 사이즈가 작아지고, 웹사이트 성능 최적화에 도움이 됩니다.

     

    웹환경에서 서브셋은 Adobe-KR-0를 따라 2780자(자모 포함 2831자)를 사용하거나,
    노민지, 윤민구 - KS 코드 완성형 한글의 추가 글자 제안(2574자, 리스트)도 참고해볼 수 있습니다.

     

    하지만 이는 자주 사용하는 글꼴만을 로딩할 수 있는 방법입니다.
    그렇다면 모든 문자을 사용하면서 필요한 부분만을 로드할 수 있을까요?

     

    unicode-range가 바로 그에 대한 대답입니다.
    알파벳처럼 문자 셋이 간단할 경우, Noto Sans와 같이 키릴문자(cyrillic), 그리스문자(greek), 라틴문자(latin) 등의 범위를 수동으로 지정하면 충분합니다.

    /* cyrillic-ext */
    @font-face {
      font-family: 'Noto Sans';
      font-style: normal;
      font-weight: 400;
      font-stretch: 100%;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosans/v35/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9X6VLKzA.woff2) format('woff2');
      unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
    }
    /* cyrillic */
    @font-face {
      font-family: 'Noto Sans';
      font-style: normal;
      font-weight: 400;
      font-stretch: 100%;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosans/v35/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9e6VLKzA.woff2) format('woff2');
      unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
    }
    /* devanagari */
    @font-face {
      font-family: 'Noto Sans';
      font-style: normal;
      font-weight: 400;
      font-stretch: 100%;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosans/v35/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9b6VLKzA.woff2) format('woff2');
      unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FF;
    }
    /* greek-ext */
    @font-face {
      font-family: 'Noto Sans';
      font-style: normal;
      font-weight: 400;
      font-stretch: 100%;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosans/v35/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9W6VLKzA.woff2) format('woff2');
      unicode-range: U+1F00-1FFF;
    }
    /* greek */
    @font-face {
      font-family: 'Noto Sans';
      font-style: normal;
      font-weight: 400;
      font-stretch: 100%;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosans/v35/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9Z6VLKzA.woff2) format('woff2');
      unicode-range: U+0370-03FF;
    }
    /* vietnamese */
    @font-face {
      font-family: 'Noto Sans';
      font-style: normal;
      font-weight: 400;
      font-stretch: 100%;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosans/v35/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9V6VLKzA.woff2) format('woff2');
      unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
    }
    /* latin-ext */
    @font-face {
      font-family: 'Noto Sans';
      font-style: normal;
      font-weight: 400;
      font-stretch: 100%;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosans/v35/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9U6VLKzA.woff2) format('woff2');
      unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
    }
    /* latin */
    @font-face {
      font-family: 'Noto Sans';
      font-style: normal;
      font-weight: 400;
      font-stretch: 100%;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosans/v35/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9a6VI.woff2) format('woff2');
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }

     

    그러나 CJK(Chinese, Japanese, Korean) 문자의 경우 일본어의 히라가나와 가타카나를 제외하면 명확한 범위를 나누기가 어렵습니다.
    우선 한자를 생각해봅시다. 모든 뜻마다 하나의 문자가 존재하죠.
    게다가 한중일 한자는 모두 조금씩 차이가 있습니다.

    한중일 한자 차이

     

    한글은 자음과 모음으로 이루어져 간단하지 않냐고요?
    그렇지 않습니다.

     

    한글의 표현방식은 모든 글자를 각각의 코드에 대응시키는 완성형과 창제 원리대로 초중종성을 합쳐 표현하는 조합형이 있습니다.
    유니코드에서는 둘 다 지원하지만, Windows나 Linux는 완성형을 주로 쓰며 MacOS는 조합형을 사용합니다.
    MacOS에서 다른 운영체제로 파일을 보내면 ㅍㅏㅇㅣㄹ.txt처럼 깨지는 원인이지요.

     

    완성형 코드

     

    완성형과 조합형의 논쟁에 대해서는 조합형/완성형/유니코드의 모든 것표준이 된 세벌식? - (4) 옛한글을 나타내는 표준 방안이 된 첫가끝 조합형을 보시면 되겠습니다.

     

    아무튼, 완성형 코드를 모두 넣기에는 폰트의 크기가 지나치게 커지게 됩니다.
    따라서 앞에서 언급한 Adobe-KR-0처럼 자주 쓰이는 글자들(2780자)만 포함하도록 만들어졌던 겁니다.

     

    그렇다면 구글은 이 문제를 어떻게 풀었을까요?
    그 방식이 아주 구글 답습니다. 바로 머신러닝!!

     

    한국어, 일본어, 중국어를 사용 패턴에 따라 서브셋을 자동으로 수행하여 제공합니다.

    /* [0] */
    @font-face {
      font-family: 'Noto Sans KR';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.0.woff2) format('woff2');
      unicode-range: U+f9ca-fa0b, U+ff03-ff05, U+ff07, U+ff0a-ff0b, U+ff0d-ff19, U+ff1b, U+ff1d, U+ff20-ff5b, U+ff5d, U+ffe0-ffe3, U+ffe5-ffe6;
    }
    /* [1] */
    @font-face {
      font-family: 'Noto Sans KR';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.1.woff2) format('woff2');
      unicode-range: U+f92f-f980, U+f982-f9c9;
    }
    /* [2] */
    @font-face {
      font-family: 'Noto Sans KR';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.2.woff2) format('woff2');
      unicode-range: U+d723-d728, U+d72a-d733, U+d735-d748, U+d74a-d74f, U+d752-d753, U+d755-d757, U+d75a-d75f, U+d762-d764, U+d766-d768, U+d76a-d76b, U+d76d-d76f, U+d771-d787, U+d789-d78b, U+d78d-d78f, U+d791-d797, U+d79a, U+d79c, U+d79e-d7a3, U+f900-f909, U+f90b-f92e;
    }
    /* ... */
    /* [117] */
    @font-face {
      font-family: 'Noto Sans KR';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.117.woff2) format('woff2');
      unicode-range: U+d, U+48, U+7c, U+ac10, U+ac15, U+ac74, U+ac80, U+ac83, U+acc4, U+ad11, U+ad50, U+ad6d, U+adfc, U+ae00, U+ae08, U+ae4c, U+b0a8, U+b124, U+b144, U+b178, U+b274, U+b2a5, U+b2e8, U+b2f9, U+b354, U+b370, U+b418, U+b41c, U+b4f1, U+b514, U+b798, U+b808, U+b824-b825, U+b8cc, U+b978, U+b9d0, U+b9e4, U+baa9, U+bb3c, U+bc18, U+bc1c, U+bc30, U+bc84, U+bcf5, U+bcf8, U+bd84, U+be0c, U+be14, U+c0b0, U+c0c9, U+c0dd, U+c124, U+c2dd, U+c2e4, U+c2ec, U+c54c, U+c57c-c57d, U+c591, U+c5c5-c5c6, U+c5ed, U+c608, U+c640, U+c6b8, U+c6d4, U+c784, U+c7ac, U+c800-c801, U+c9c1, U+c9d1, U+cc28, U+cc98, U+cc9c, U+ccad, U+cd5c, U+cd94, U+cd9c, U+cde8, U+ce68, U+cf54, U+d0dc, U+d14c, U+d1a0, U+d1b5, U+d2f0, U+d30c, U+d310, U+d398, U+d45c, U+d50c, U+d53c, U+d560, U+d568, U+d589, U+d604, U+d6c4, U+d788;
    }
    /* [118] */
    @font-face {
      font-family: 'Noto Sans KR';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.118.woff2) format('woff2');
      unicode-range: U+39, U+49, U+4d-4e, U+a0, U+ac04, U+ac1c, U+ac70, U+ac8c, U+acbd, U+acf5, U+acfc, U+ad00, U+ad6c, U+adf8, U+b098, U+b0b4, U+b294, U+b2c8, U+b300, U+b3c4, U+b3d9, U+b4dc, U+b4e4, U+b77c, U+b7ec, U+b85d, U+b97c, U+b9c8, U+b9cc, U+ba54, U+ba74, U+ba85, U+baa8, U+bb34, U+bb38, U+bbf8, U+bc14, U+bc29, U+bc88, U+bcf4, U+bd80, U+be44, U+c0c1, U+c11c, U+c120, U+c131, U+c138, U+c18c, U+c218, U+c2b5, U+c2e0, U+c544, U+c548, U+c5b4, U+c5d0, U+c5ec, U+c5f0, U+c601, U+c624, U+c694, U+c6a9, U+c6b0, U+c6b4, U+c6d0, U+c704, U+c720, U+c73c, U+c740, U+c744, U+c74c, U+c758, U+c77c, U+c785, U+c788, U+c790-c791, U+c7a5, U+c804, U+c815, U+c81c, U+c870, U+c8fc, U+c911, U+c9c4, U+ccb4, U+ce58, U+ce74, U+d06c, U+d0c0, U+d130, U+d2b8, U+d3ec, U+d504, U+d55c, U+d569, U+d574, U+d638, U+d654, U+d68c;
    }
    /* [119] */
    @font-face {
      font-family: 'Noto Sans KR';
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url(https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.119.woff2) format('woff2');
      unicode-range: U+20-22, U+27-2a, U+2c-38, U+3a-3b, U+3f, U+41-47, U+4a-4c, U+4f-5d, U+61-7b, U+7d, U+a1, U+ab, U+ae, U+b7, U+bb, U+bf, U+2013-2014, U+201c-201d, U+2122, U+ac00, U+ace0, U+ae30, U+b2e4, U+b85c, U+b9ac, U+c0ac, U+c2a4, U+c2dc, U+c774, U+c778, U+c9c0, U+d558;
    }

     

    저는 구글의 머신러닝 결과를 날먹(?)하여 블로그에 적용할 폰트에 사용하고 싶었습니다.
    따라서 CSS의 unicode-range 부분만을 파싱하여 서브셋해주는 라이브러리를 만들었죠.
    font-range의 탄생배경입니다.

     

    CSS 파일 경로나 URL을 입력하면 알아서 다운로드 받아 unicode-range에 따라 서브셋 해줍니다.

    fontRange(font_path, css_url_or_path);
    fontRange(font_path, css_url_or_path, save_dir);       // Option1
    fontRange(font_path, css_url_or_path, { ...options }); // Option2

     

    물론 텍스트를 이용한 서브셋까지 모두 지원합니다.

    fontSubset(font_path);
    fontSubset(font_path, save_dir);       // Option1
    fontSubset(font_path, { ...options }); // Option2

     

    그리고, 매번 호출할 필요 없이 한번에 효율적인 서브셋을 할 필요가 생겨서 만들어진 API가 있으니 fontPipe입니다.

    fontPipe([
      { font_path },                                      // As `fontSubset(font_path)`
      { font_path, option: { text: "abc" } },             // As `fontSubset(font_path, { text: "abc" })`
      { font_path, option: { textFile: file_path } },     // As `fontSubset(font_path, { textFile: file_path })`
      { font_path, option: { cssFile: css_url_or_path } } // As `fontRange(font_path, css_url_or_path)`
    ]);
    fontPipe([{ font_path1 }, { font_path2 }], "<index>/<total>"); // Sharding option use like `1/2`

     

    효율적인 서브셋하기

    font-range의 fontPipe()는 얼마나 효율적일까요?

     

    제가 구현한 프리텐다드의 고성능 서브셋 시스템을 보면 그 위력을 알 수 있습니다.

    기존 서브셋
    고성능 서브셋

     

    한시간 동안 걸리던 작업이 겨우 5분으로 줄어들었습니다!!!
    수치상 11.14배가 빨라진거죠.

     

    대체 어떻게 이런 일이 가능했을까요?
    그 비밀을 파고들어봅시다.

     

    쉘 생성 제거

    먼저 서브셋 작업을 어떻게 하나 알아봅시다.
    저는 이미 만들어진 서브셋 유틸리티를 사용했습니다.

     

    파이썬의 폰트툴즈(fonttools) 패키지를 설치하면 제공되는 pyftsubset입니다.
    보통 이렇게 Shell 명령어로 사용합니다.

    pyftsubset "NotoSansCJKkr-Regular.otf" \
      --flavor="woff2" \
      --output-file="./subset-fonts/NotoSansR.subset.5.woff2" \
      --text-file="glyphs.txt" \
      --layout-features='*' \
      --glyph-names \
      --symbol-cmap \
      --legacy-cmap \
      --notdef-glyph \
      --notdef-outline \
      --recommended-glyphs \
      --name-legacy \
      --drop-tables= \
      --name-IDs='*' \
      --name-languages='*'

     

    Node.JS에는 명령어를 실행하기 위한 방법으로 execspawn이 있습니다.
    exec는 bash와 같은 쉘을 이용해 실행하므로 자동적으로 이스케이프 처리가 되어, spawn 보다 편리합니다.
    복잡한 옵션 명령어들을 다루어야 했던 제게 exec가 가진 이점이 컸습니다.

     

    서브셋 해야할 폰트가 적었던 초반에는 엄청나게 커다란 문제가 되지 않았지만, pretendard-jppretendard-std처럼 새로운 폰트들이 추가되고 가변폰트의 다이나믹 서브셋까지 지원하면서 엄청난 양의 서브셋이 만들어지게 됩니다.
    용량이 너무 커서 NPM에 올라가지 못해 반쯤 강제로 패키지를 나누어 관리하는 모노레포 작업까지 했었어야 했죠.

     

    서브셋이 생성되는 조건을 몇가지 적어보도록 하겠습니다.

    1. 폰트 패밀리: pretendard, pretenard-std, pretendard-jp와 최근 추가된 pretendard-gov처럼 다양한 폰트 바리에이션들
    2. 굵기: 프리텐다드는 9개의 weight를 지원합니다. 모든 굵기가 합쳐져 옵션으로 조정하는 가변폰트도 있습니다.
    3. 포맷: woff와 woff2 형식으로 나뉩니다.
    4. 서브셋 범위: 전체, Adobe-KR-0, unicode-range를 이용한 다이나믹 서브셋

    특히 다이나믹 서브셋은 각 폰트파일당 90개가 넘게 나뉘어집니다.
    단순히 곱해서 계산을 해보면 수천개의 서브셋이 만들어져야 합니다.

     

    때문에 Pretendard 스케일에서는 쉘 생성 제거는 효용을 가지게 될만하다 판단해 쉘 생성을 제거해보기로 결정했습니다.
    대신 이스케이프처리와 같이 편리함은 포기하기 싫었어요.

     

    그래서 execa라는 라이브러리를 사용하였습니다.
    execa 자체는 esm 모듈을 사용하고 있어 esm2cjs/execa로 설치했고요.

    지금 벤치마크 결과는 없지만 당시 기억으로 8%정도의 개선이 있었습니다.

     

    워커 풀

    이번에는 변환 작업 자체의 속성을 생각해보도록 합시다.
    작업의 속성에 따라 개선 방향이 달라지기 때문이지요.

     

    작업의 속성을 크게 2가지로 나누자면 CPU와 I/O 작업이 있습니다.

    CPU bound vs I/O bound

     

    만약 CPU 중심 작업이라면 병렬성(parallel), I/O 중심 작업이라면 동시성(concurrent)에 신경을 써야하겠지요.

    동시성 vs 병렬성

     

    서브셋 작업은 어디에 속할까요?
    파일을 읽고 쓰기는 분명 I/O겠지만, 명령이 실행되는 동안 오랫동안 기다려야 하는 변환작업은 CPU를 계속 점유해야 가능합니다.
    CPU Bound 작업이라는거죠.

    하지만 자바스크립트는 싱글 스레드 언어이며, Node.JS도 마찬가지의 한계를 가지고 있습니다.

    싱글쓰레드

     

    JS의 await는 I/O에는 뛰어나지만, CPU Bound일때는 다른작업들이 차단(block)되어 실행되지 않습니다.
    다행히도 당시 코드사용해보면 여러 Promise가 반환되어 Promise.all()로 처리할 수 있었습니다.
    완전히 멈춰있는 최악의 상황은 아니었지요.

      return ranges.then(eachRanges => eachRanges.map((unicodes, i) => {
        const saveOption = "--output-file='" +
          join(dirPath, getName(nameFormat, fontName, i, fontExt)) + "' ";
        const unicodeRanges = unicodes.split(', ').join(',');
        const unicodeOption = "--unicodes='" + unicodeRanges + "' ";
    
        const options = " '" + fontPath + "' " + saveOption + unicodeOption
          + convertOption + defaultOption + etcOption;
        return execSync("pyftsubset" + options);
      }));

     

    하지만 수십개의 프로세스가 동시에 나타납니다.
    OS는 모든 프로세스를 공평하게 실행시켜줘야 하기 때문에 작업을 전환시킵니다.

     

    그러면 이른바 컨텍스트 스위칭이 일어나게 되며, 성능이 저하됩니다.
    여러가지 일을 번갈아가며 하느라 집중이 자꾸 깨지는 상태인 거죠.

    컨텍스트 스위칭

     

    그럼 어떻게 하는게 좋을까요?
    해결할 수 있는 방법 중 하나는 CPU 코어 갯수대로만 쓰레드를 만들고 큐 형태로 관리하여 일을 배분해주는 겁니다.
    점유가 끝나야 다음 작업을 시작하므로 컨텍스트 스위칭이 최소화됩니다.

    쓰레드 풀

     

    Piscina라는 워커 풀을 사용하였습니다.
    자바스크립트 세계에서 CPU 바운드 작업은 Worker를 사용하면 됩니다.

    Worker

     

    워커 풀을 사용 시 몇가지 실수하거나 헷갈릴만한 것들이 존재합니다.
    첫번째로 워커는 일반적으로 다른 script 파일이어야 하므로 worker.ts에서 명령이 실행되어야 하죠.

    두번째, 명령 실행을 동기적으로 해야합니다.

    export default function subset({options, log = ""}: SubsetI) {
      if(log !== "") {
        console.log(log);
      }
      return execaSync("pyftsubset", options);
    }

     

    왜 그럴까요?
    바로 자바스크립트 언어 특성 때문입니다.

     

    만약 비동기로 실행하게 되면 async를 만나는 순간 프로미스로 반환해버리고, 작업이 끝난 취급을 받아 다음 작업으로 넘어가게 되어 워커풀을 도입하기 전과 동일하게 수많은 프로세스가 생기게 됩니다.

    await 실행

     

    마지막으로 싱글턴 패턴사용하여 워커풀이 처음 한번만 초기화되도록 보장했습니다.

    type WorkerRT = ReturnType<typeof WorkerFn>;
    
    class Worker {
      private static instance: Piscina;
      private constructor() { }
    
      public static getInstance(): Piscina {
        if(!Worker.instance) {
          Worker.instance = new Piscina({
            filename: join(__dirname, "../", "build", "worker.js")
          });
        }
        return Worker.instance;
      }
    }

     

    워커 풀은 약 20%의 성능 개선을 가져왔던 걸로 기억합니다.

    샤딩

    샤딩(Sharding)은 주로 DB쪽에서 사용되는 용어로 분산하여 저장하는 프로세스입니다.

     

    구현은 정말 간단합니다.
    예를 들어 10개의 작업이 있을 때 2/5와 같은 옵션을 주면 2/5의 양만큼만 처리하므로 34만 실행되는 방식입니다.

     

    그렇다면 사용시 고려해야하는 점은 있을까요?
    당연히 몇가지 있습니다.

     

    첫번째, "1/5"~"5/5"까지 처리한 작업물들을 모아서 합쳐야 합니다.
    저는 커밋을 만들어 push하고 rebase하는 형식으로 처리했습니다.

    runs:
      using: "composite"
      steps:
        - name: Commit Build File
          shell: bash
          run: |
            git config user.name "$(git log -n 1 --pretty=format:%an)"
            git config user.email "$(git log -n 1 --pretty=format:%ae)"
            if [[ "$(git branch -r --contains ${{ inputs.branch }} 2>/dev/null)" ]]; then
              git checkout ${{ inputs.branch }}
            else
              git checkout -b ${{ inputs.branch }}
            fi
            git add --ignore-removal ${{ inputs.file_pattern }}
            if [[ "$(git diff --staged)" != "" ]]; then
              git commit -m "${{ inputs.message }}"
              if [[ "$(git branch -r --contains ${{ inputs.branch }} 2>/dev/null)" ]]; then
                git push origin ${{ inputs.branch }}
              else
                git push -u origin ${{ inputs.branch }}
              fi
            fi

     

    , 분산할 수 있는 시스템에는 한계가 있을 수 있습니다.
    깃허브의 경우 동시에 실행가능한 작업은 20개이며, MacOS는 5개입니다.
    Linux는 2코어이지만 MacOS는 현재 3코어, 4코어도 지원하므로 성능 측정을 해야합니다.

     

    당시에는 3코어짜리 MacOS만 존재했는데, 3코어 MacOS 5개 대신 2코어 Linux 8개를 사용하는 성능 특성이 좋아 모두 리눅스 머신을 기준으로 해서 처리하였습니다.

      pretendard-jp:
        runs-on: ubuntu-latest
        if: contains(github.event.head_commit.message, 'release:')
        strategy:
          matrix:
            shard: ["1/8", "2/8", "3/8", "4/8", "5/8", "6/8", "7/8", "8/8"]
            include:
              - shard: "1/8"
                branch: "jp-1"
              - shard: "2/8"
                branch: "jp-2"
              - shard: "3/8"
                branch: "jp-3"
              - shard: "4/8"
                branch: "jp-4"
              - shard: "5/8"
                branch: "jp-5"
              - shard: "6/8"
                branch: "jp-6"
              - shard: "7/8"
                branch: "jp-7"
              - shard: "8/8"
                branch: "jp-8"
        steps:
        - uses: actions/checkout@v3
          with:
            fetch-depth: '0'
        - uses: ./.github/actions/setup-pip
        - uses: ./.github/actions/setup-yarn
        - uses: ./.github/actions/subset-push
          with:
            workspace: pretendard-jp
            shard: ${{ matrix.shard }}
            branch: ${{ matrix.branch }}

     

    세번째, 불균일한 작업시간입니다.
    모든 문자를 woff2로 변환해야하는 static 작업은 문자 집합의 일부분만 변환하면 되는 다이나믹 서브셋의 각 작업보다 느립니다.

     

    font-range 내부에서는 dynamicunicode-range들을 각각 작업으로 처리하고 있습니다.

    그래서 전 오래걸리는 static 작업끼리 모아놓기보다는 순서를 적당히 섞는 간단한 휴리스틱으로 Normalizing을 했습니다.

    // 전
    subsets(
      // Pretendard
      ["static",    "woff",  fontList],
      ["static",    "woff2", fontList],
      ["glyph",     "woff",  fontList],
      ["glyph",     "woff2", fontList],
      ["dynamic",   "woff",  fontList],
      ["dynamic",   "woff2", fontList],
      // Pretendard Variable
      ["static",    "woff2", variable],
      ["dynamic",   "woff2", variable]
    );
    
    // 후
    subsets(
      // Pretendard woff
      ["static",    "woff",  fontList],
      ["glyph",     "woff",  fontList],
      ["dynamic",   "woff",  fontList],
    
      // Pretendard woff2
      ["static",    "woff2", fontList],
      ["glyph",     "woff2", fontList],
      ["dynamic",   "woff2", fontList],
    
      // Pretendard Variable
      ["static",    "woff2", variable],
      ["dynamic",   "woff2", variable]
    );

     

    물론 Shuffle하는 방식도 생각해보았지만, 결정적이어야하는 Sharding 시스템 특성상 어울리지 않았습니다.
    그래서 seed 값을 사용하는 방안(seedrandom, seed-shuffle)도 생각해보았으나 너무 오버엔지니어링에 테스팅도 까다로워질 것 같아 시도해보지 않았어요.

    대신 나중에 필요하면 한번즈음 시도는 해볼 것 같네요.

     

    좋습니다. 그래서 성능은 어떨까요?
    10배가 넘는 성능을 만든 1등공신입니다!!

    이렇게 훌륭한 일을 하지만, 놀랍게도 font-range의 소스코드는 500라인에 불과합니다.
    가독성도 좋은 편이니 시간이 있으신 분들은 한번쯤 읽어보시는 것도.

     


     

    위가 원래 내용으로 주말에 작성하여 다른 팀의 직원의 문서변환 작업에 우선적으로 도움을 주기 위한 용도 였습니다.

    그러나...ㅠㅠ 변환과 성능 최적화를 제가 맡게 되면서 여러가지 작업을 하게 되었는데요.

     

    1. 20만건이 넘는 데이터를 PDF로 변환하기: 제가 참여하기 전보다 200% 이상의 성능개선!!
      • 위의 쉘 생성 제거와 워커풀 활용
      • Puppeteer 또는 Selenium 대신 성능/안정화를 위해 패치한  JSDOM
      • 역시 성능/안정화를 위해 프로미스 풀
      • 파일 건너뛰기와 로컬 이미지 사용
      • Stream I/O
    2. 일부 엑셀 파일을 PDF로 변환하기
      • 헤드리스 모드의 LibreOffice에서 출력용 VBA를 적용 후, PDF 생성
      • CUPS와 같은 패키지가 추가로 필요할 수도 있음
    3. 웹사이트에서 PDF 뷰어 제공하기
      • PDF.JS는 생각보다 커스텀하기 까다롭게 되어 있다
      • 서버의 헤더를 요구하거나 .mjs등의 확장자 때문에 추가적인 리소스 허용등의 설정이 필요할 수도.

     

    사실 생각나는대로 위보다 자세하게 추가적인 메모는 해두었는데..

    이 정도가 NDA에서 어긋나지 않는 선에서 공개할 수 있는 내용인듯.

    댓글

Designed by black7375.