हमने JavaScript monorepo का Git आकार 94% कैसे घटाया
(jonathancreamer.com)- Microsoft का बड़ा JavaScript monorepo 1JS मासिक सक्रिय 1,000 उपयोगकर्ताओं, करीब 2,500 packages और लगभग 2 करोड़ lines of code तक बढ़ चुका था, और हाल में clone size 178GB तक पहुंच गया था, जिससे कुछ developers के लिए clone करना ही मुश्किल हो गया था
- शुरुआती bottleneck Beachball change files की वह structure थी जिसमें एक folder में दसियों हजार files जमा हो रही थीं; files 40,000 तक बढ़ने पर हर नई file जोड़ने पर बड़ा tree object बन रहा था
- बड़ा कारण यह था कि
CHANGELOG.mdऔरCHANGELOG.jsonजैसे लंबे filenames बार-बार update हो रहे थे, और Git का पुराना packing code delta target चुनते समय filename के आखिरी 16 अक्षर ही देख रहा था git repack -adf --window=250भी असरदार रहा, लेकिन Microsoft Git fork2.47.0.vfs.0.2में path-based packinggit repack -adf --path-walkचलाने पर local repository 178GB से 5GB रह गई- बड़े monorepo में लंबे नाम वाली files अक्सर बदलती हैं, तो
pack.usePathWalk trueऔर नयाgit surveycommand जांचना चाहिए; संबंधित बदलाव Git upstream में भी शामिल किए जाने की योजना है
1JS monorepo का पैमाना और bottleneck
- Microsoft के अंदर का बड़ा JavaScript monorepo 1JS सिर्फ repository size में ही नहीं, code और contributors के पैमाने में भी बड़ा है
- 1,000 से ज्यादा monthly active users
- करीब 2,500 packages
- लगभग 2 करोड़ lines of code
- हाल में clone की गई repository का size 178GB था, और कुछ European developers size की वजह से repository clone नहीं कर पा रहे थे
- पहली बार clone करते समय यह 1–2GB के आसपास था, लेकिन कुछ महीनों बाद करीब 4GB तक बढ़ गया और उसके बाद भी लगातार बढ़ता रहा
Lesson #1: एक folder में हजारों files जमा न करें
git-sizerने बड़े blobs और Beachball change files की समस्या उजागर की- बड़े blobs आम तौर पर binaries को गलती से commit करने पर बनते हैं, और Azure DevOps की check-in size limit जैसी सुविधाओं से इन्हें रोका जा सकता है
- जो files पहले ही history में जा चुकी हों, उन्हें बाद में हटाना आसान नहीं होता
- 1JS ने packages की semver range increments को automate करने के लिए Beachball change files का उपयोग किया
- इसका उद्देश्य Changesets जैसा है
- लक्ष्य semantic-release की तरह package version increments को automate करना है
- change files delete नहीं की गईं, जिससे एक folder में 40,000 तक files जमा हो गईं, और ऐसी स्थिति में नई file जोड़ने पर बड़ा tree object बनता है
- इसके लिए दो उपाय किए गए
- Beachball PR के जरिए package-wise एक file के बजाय कई changes को एक single change file में रखने के लिए संशोधन किया गया
- change files folder को समय-समय पर अपने आप साफ करने वाली pipeline लिखी गई
Lesson #2: versioned branch और changelog packing की समस्या
- 1JS का version management flow
versionedbranch बनाए रखता है, जोmainकी mirror है- यह actual package versions को store करता है ताकि
mainमें Git conflicts से बचा जा सके - इससे NPM package के रूप में release हुए semver versions और Git commits के बीच का संबंध ठीक-ठीक देखा जा सकता है
- यह actual package versions को store करता है ताकि
versionedbranch में मुख्य रूप सेCHANGELOG.mdऔरCHANGELOG.jsonfiles में content जोड़ने वाले commits ही थे, लेकिन यह इतना बढ़ गया कि clone करना धीरे-धीरे मुश्किल होने लगा- 2021 के बाद repository में हर साल लाखों lines of code और सैकड़ों developers जुड़ते गए, इसलिए उसका बढ़ना स्वाभाविक था; लेकिन जब growth rate Microsoft के Office monorepo से भी ज्यादा हो गया, तो अलग कारण पर शक हुआ
- Git के
shallow checkout,sparse indexजैसे बड़े monorepo से जुड़े features बनाने वाले developer ने जांच की, तो पता चला किversionedbranch fetch करते समय 125GB अतिरिक्त Git data fetch हो रहा था
Git packing code गलत delta कैसे बना रहा था
- कारण यह था कि पुराना Git packing code files को compress करने से पहले compare करते समय filename के आखिरी 16 अक्षर ही check करता था
- आम तौर पर Git बदली हुई file का सिर्फ diff push करता है, लेकिन इस packing problem की वजह से अलग-अलग packages की
CHANGELOG.mdfiles आपस में compare हो रही थीं - उदाहरण के लिए, अगर
repo/packages/foo/CHANGELOG.jsonबदला जाए, तो Git push की तैयारी के दौरानrepo/packages/bar/CHANGELOG.jsonके साथ diff बना सकता था - इसकी वजह से कई मामलों में पूरी file बार-बार फिर से push हो रही थी, और कुछ files दर्जनों MB आकार की हो सकती थीं
- समस्या की details Stolee के Git mailing list post में संक्षेप में दी गई हैं
repack और path-walk के परिणाम
- पहले
git repack -adf --window=250से ज्यादा बड़ा window इस्तेमाल करके pack files की compression बेहतर की गई, और repository size काफी घट गया - इसके बाद git-for-windows PR #5171 ने commits walk करने के default तरीके के बजाय Git paths walk करने वाला packing method जोड़ा
- Microsoft के Git fork Microsoft git
git version 2.47.0.vfs.0.2में नए तरीके से clone करने के बाद repack चलाया गया- चलाया गया command:
git repack -adf --path-walk - परिणाम: repository size 178GB से 5GB हो गया
- चलाया गया command:
- नया configuration option push के समय भी सही deltas generate करवाता है
git config --global pack.usePathWalk true
git version 2.47.0.vfs.0.2इस्तेमाल करने वाले developers local clone के बाद repack चला सकते हैं, और नए path-walk push algorithm से growth rate कम कर सकते हैं
GitHub, Azure DevOps और upstream Git पर असर
- GitHub समय-समय पर repack और Git garbage collection करता है, लेकिन GitHub का मौजूदा packing method
CHANGELOG.md,CHANGELOG.jsonजैसी files के deltas सही तरह calculate नहीं कर पाता - जिन लंबे filenames के आखिरी 16 से ज्यादा अक्षर समान हों और जो समय के साथ बार-बार बदलते हों, वे भी प्रभावित हो सकते हैं
- उदाहरण के तौर पर i18n के लिए बड़ी string files का जिक्र किया गया
- 1JS जिस Azure DevOps का इस्तेमाल करता है, वह अभी इस तरह का repack नहीं करता, और server-side repository size घटाने के लिए संबंधित काम जारी है
- संबंधित बदलाव Git upstream में भी शामिल किए जाने की योजना है
बड़े monorepo में क्या जांचें
- बड़े monorepo में अगर
CHANGELOG.mdया अपेक्षाकृत लंबे नाम वाली files बार-बार update होती हैं, तो path-walk से जुड़े बदलावों पर नजर रखना जरूरी है - नया
git surveycommand repository की स्थिति समझने के लिए heuristics देता है- Top Files By Disk Size
- Top Directories By Inflated Size
- Top Files By Inflated Size
- ये heuristics यह अंदाजा लगाने में मदद करते हैं कि path-walk का काम उस repository size पर भी असर डालेगा या नहीं
1 टिप्पणियां
Hacker News की राय
नया
git-surveycommand कहाँ है, तो यह अभी git.git में शामिल नहीं हुआ हैलेखक Microsoft के Git fork का इस्तेमाल कर रहे हैं, और यह command इस गर्मी में जोड़ा गया था: https://github.com/microsoft/git/pull/667
full-name-hashऔरpath-walkभी सिर्फ fork में या Git HEAD में ही दिखते हैंmanual page या 2.47 changelog में नहीं मिलते
यह जानने की जिज्ञासा है कि “यूरोप में लोग repository size की वजह से clone भी नहीं कर पा रहे” से क्या मतलब है
यह अजीब लगता है कि कोई बड़ा repository यूरोप में fetch नहीं किया जा सकता लेकिन कहीं और किया जा सकता है, और server-side बदलाव लागू होने तक तो ऐसा लगता है कि वे फिर भी clone नहीं कर पाएंगे
“पूरी file को बार-बार दोबारा push किया” वाला वाक्य बीच में कटा हुआ लगता है, और article पढ़ते समय GIF बहुत distract कर रहा था और reading mode में भी वैसा ही दिख रहा था
अगर उसे ज्यादा पूरा लिखें तो शायद मतलब यह होगा: “कोई व्यक्ति यूरोप में travel करते समय इस्तेमाल के लिए laptop पर पूरा repository clone करना चाहता था, लेकिन उस समय disk full होने की वजह से नहीं कर पाया। बाद में disk साफ करके clone कर सका”
मुझे नहीं लगता कि इसका मतलब यह है कि यूरोप 180GB file संभाल नहीं सकता
खासकर अगर संबंधित व्यक्ति VPN, और उसमें भी भयानक “SSL VPN” workaround इस्तेमाल कर रहा हो; और खराब latency handling वाला software या टूटी हुई window scaling वाले server आज भी हैरानीजनक रूप से आम हैं
अगर ज्यादातर developer server के पास हों और अच्छी bandwidth वाली connection इस्तेमाल कर रहे हों, तो ऐसी समस्या आसानी से सामने नहीं आती
Azure DevOps clone wired gigabit connection को बिल्कुल saturate नहीं करता, और इसमें इतना समय लग जाता है कि आखिरकार मेरी तरफ या Azure DevOps की तरफ से रुक जाता है और हार माननी पड़ती है
अच्छी बात है कि लगभग सारा काम Codespaces के अंदर shallow clone से हो जाता है, इसलिए यह बड़ा issue नहीं है; और अगर हमारे repository में भी इस article के 1JS repository जैसी size explosion है, तो उम्मीद है वह fix हो जाएगी
अपडेट: यह एक बेवकूफी भरी गलती थी, और फ़ाइल नाम में पूरा path शामिल नहीं होता
लगता है explanation GIFs के बीच दब गया था, लेकिन यहाँ आख़िरी 16 अक्षर अलग-अलग हैं: उदाहरण यह है कि
repo/packages/foo/CHANGELOG.mdबदला गया, लेकिन Git ने push की तैयारी के दौरानrepo/packages/bar/CHANGELOG.mdसे compare करके diff बनायाpath-walkAPI cover letter भी देखने लायक है: https://lore.kernel.org/all/pull.1786.git.1725935335.gitgitg...ब्लॉग का उदाहरण बहुत साफ़ नहीं है, लेकिन Git असल में repository के अंदर सभी files के सभी versions लाकर path के आख़िरी 16 bytes को hash table में डाल रहा था, और जिन चीज़ों को वही file के अलग-अलग versions समझने की उम्मीद थी उन्हें delta compression के लिए group कर रहा था
लेख में
foo/CHANGELOG.mdऔरbar/CHANGELOG.mdकेवल 13 अक्षर हैं, इसलिए example ज्यों का त्यों सही नहीं बैठता; किसी ऐसे path की कल्पना करनी होगी जिसमें लंबा common suffix होयह हिस्सा
--full-name-hashoption से ठीक किया गया है, ताकि अब 16 bytes नहीं बल्कि पूरा path compare होइसके बाद जिस window size बढ़ाने की बात की गई है, वह खराब file grouping को bypass करने वाला hack जैसा है, असली fix नहीं
compressor को अब भी बेहद खराब input देकर और बहुत ज़्यादा memory इस्तेमाल करके bypass करने जैसा है, इसलिए इसे solution की तरह पेश करना confusing था
सच में दिलचस्प हिस्से path-walk API और
--full-name-hashहैंयहाँ file name से मतलब path है या Git एक ही नाम वाली कोई भी file चुनकर diff बना रहा है, यह जानने की जिज्ञासा है
यह भी जानना चाहूँगा कि दूसरी file चुनने के तरीके में कोई pattern है या नहीं
--full-name-hashPR का पहला commit message देख सकते हैं: https://github.com/git-for-windows/git/pull/5157/commits/d5c...अंश के तौर पर,
"/CHANGELOG.json"15 अक्षर का है और beachball tool इसे बनाता है; parent directory का केवल आख़िरी अक्षर ही इस file के अलग-अलग versions को distinguish कर सकता है, और उसमें भी सिर्फ़ top 2 bits ही reflect होते हैंअगर वह अक्षर letter है तो हमेशा collision होगा, और
"/CHANGELOG.md"path में भी मिलती-जुलती समस्या है, हालांकि parent directory से फर्क पड़ने की थोड़ी ज़्यादा गुंजाइश हैgrouping algorithm नाम के दाईं तरफ़ से दूर जाते-जाते हर character को कम weight देता है:
hash = (hash >> 2) + (c << 24)hash 32-bit है, और पूरे path के हर 8-bit character को क्रम से hash के सबसे ऊपरी 8 bits में जोड़ता है; लेकिन existing hash bits को दाईं ओर 2 bits shift करने के कारण final hash पर केवल आख़िरी 16 characters का असर पड़ता है
असल में क्या होता है, यहाँ देखा जा सकता है: https://go.dev/play/p/JQpdUGXdQs7
Go में port करके
"aaa/CHANGELOG.md"और"zzz/CHANGELOG.md"की final values compare की हैं, इसलिएaaaऔरzzzमें अलग-अलग values डालकर देखेंगे तो दिखेगा कि final value पर उनका कितना कम असर होता हैCHANGELOG.mdके आख़िरी 16 characters ही पूरा file name हैं, और इस तरह समझें तोfilepathwalksolution समस्या को क्यों हल करता है, यह भी explain होता हैलेकिन अगर सच में आधार केवल file name के आख़िरी 16 characters हैं और पूरा path नहीं, तो खासकर monorepo में यह समस्या कहीं ज़्यादा common होनी चाहिए थी
शायद
fooऔरbarजैसे meta variables की वजह से confusion हुआ, और असली package names मेंfirstPkg,secondPkgजैसा common suffix रहा होगासिर्फ़
"/CHANGELOG.md"ही 13 characters है, इसलिए common suffix के 3 characters भी हों तो 16-character collision हो जाता हैमैंने इसे तुरंत
nixpkgsपर test किया; GitHub से ज्यों का त्यों clone करने पर यह लगभग 5GB हैलेख में बताए गए पहले option
--window 250ने size को घटाकर 1.7GB कर दिया, और Microsoft Git fork का नया--path-walkoption कम effective रहा, जिससे total 1.9GB हुआदोनों ही initial size के आधे से कम हैं, इसलिए अच्छा होगा अगर GitHub के पास ऐसे काम run करने का कोई तरीका हो; और उससे भी बेहतर होगा अगर लोग ऐसी चीज़ों को सीधे control कर सकने वाले तरीके से hosting शुरू करें
लेख में Derrick Stolee का ज़िक्र है, जिन्होंने ज़रूरी बदलावों को dig into करके release किया
अगर Git internals, local और CI में Git clone size घटाने जैसी चीज़ों में रुचि है, तो Derrick ने GitHub blog पर कई बेहतरीन posts लिखी हैं: https://github.blog/author/dstolee/
उनकी personal site भी देखने लायक है: https://stolee.dev/
अगर गलती से कोई बड़ा binary commit हो गया हो, तो सुधार का तरीका यह माना जा सकता है कि
filter-branchचलाकर उस problematic binary को हटाया जाए, सभी को binary के लिए git-lfs इस्तेमाल करना सिखाया और configure किया जाए, फिर force push करके सबके working environment को सामान्य स्थिति में वापस लाया जाएयह ideal नहीं है, लेकिन ऐसे बड़े file को Git में बनाए रखने से बेहतर है जिसका इस्तेमाल भी नहीं हो रहा
filter-branchडरावना लगता है, उनके लिए BFG भी है: https://rtyley.github.io/bfg-repo-cleaner/जैसा किसी और ने बताया, यह समस्या छोटे लेकिन अक्सर बदलने वाले files की है, इसलिए space बचाने के लिए history से पुराने versions हटाकर आगे से LFS इस्तेमाल करना चाहिए
Git को hack करना मजेदार हो सकता है, लेकिन सोचता हूं कि क्या शुरुआत में ही monorepo में 2,500 packages न डालने का कोई तरीका नहीं था
codebase जितना बड़ा होता है, उतना ही यह उम्मीद करना मुश्किल हो जाता है कि लोग पुराना wheel फिर से invent नहीं करेंगे, क्योंकि या तो उन्हें उस code के मौजूद होने का पता नहीं होता, या वे अपने use case को handle करने के लिए उसे बदलते हुए मौजूदा usages तोड़ देने से डरते हैं
अनुभव से लगता है कि 50 modules से 100 modules तक जाने की तुलना में 100 से 200 तक जाने में कम समय लगता है
यह पूरी तरह खुद पैदा की गई समस्या लगती है
हाल में GitButler द्वारा पोस्ट किए गए GitMerge presentations देखकर monorepo और scalability से जुड़े developments follow कर रहा हूं, और Microsoft, GitHub, GitLab से कई अच्छी चीजें आ रही हैं
मैं और समझना चाहता हूं कि आखिरी 16 characters check और full path check की समस्याएं delta compression, pack index और multi-pack index के साथ कैसे जुड़ती हैं
यह देखकर अच्छा लगा कि Microsoft खुद Azure DevOps इस्तेमाल कर रहा है
लगता था कि ज्यादा से ज्यादा Azure services सिर्फ GitHub के लिए native connector दे रही हैं, इसलिए Azure DevOps को शायद छोड़ दिया जा रहा है
Git के internals को इतनी अच्छी तरह जानने वाला कोई व्यक्ति हाथ की पहुंच में हो और मदद कर सके, इस scale की company में ऐसा project करते समय यह वाकई बड़ा perk होगा