- Wild เป็นลิงเกอร์ที่มุ่งให้การลิงก์ระหว่างการพัฒนาแบบวนซ้ำทำได้เร็วมาก แม้ยังไม่ได้ implement การลิงก์แบบ incremental แต่การลิงก์แบบ non-incremental เพียงอย่างเดียวก็อยู่ในระดับที่เร็วแล้ว
- mold ซึ่งเป็นลิงเกอร์ความเร็วสูงที่มีอยู่เดิม ไม่ทำ incremental link และไม่มีแผนจะทำ จุดนี้จึงเป็นจุดเริ่มต้นของ Wild โดย Wild เขียนด้วย Rust และมีเป้าหมายเพื่อรับมือกับความซับซ้อนของ incremental linking
- แพลตฟอร์มที่รองรับในปัจจุบันคือ x86-64, ARM64, RISC-V, LoongArch64, PPC64LE บน Linux และรองรับ static binary, static-PIE, binary แบบ dynamic link, shared object, Rust proc-macro, debug information เป็นต้น
- สามารถใช้เป็น drop-in replacement ที่ GCC·Clang เรียกเหมือนลิงเกอร์เดิมได้ และเชื่อมใช้กับ Cargo, ระบบ build ของ C/C++, และ wild-action สำหรับ CI ได้
- ยังไม่ได้ implement incremental linking, linker script ที่ซับซ้อนกว่า, การรองรับ Mach-O และ Windows และต้องตรวจสอบว่าถูกใช้ลิงก์จริงหรือไม่ด้วย
readelf หรือ strings โดยดูข้อความ Linker: Wild version... ใน binary
เป้าหมายและตำแหน่งของ Wild
- Wild เป็นลิงเกอร์ที่มุ่งให้การลิงก์ระหว่างการพัฒนาแบบวนซ้ำทำได้เร็วมาก
- แผนระยะยาวคือ implement incremental linking แต่ปัจจุบันยังไม่ได้ implement
- ระบุว่าแม้ไม่มี incremental linking ประสิทธิภาพของ non-incremental linking ก็เร็วอยู่แล้ว
- mold นั้นเร็วมากอยู่แล้ว แต่ไม่ทำ incremental linking และผู้เขียนระบุว่าไม่มีแผนจะทำ incremental linking นี่คือเหตุผลที่ Wild ถูกสร้างเป็นลิงเกอร์แยกต่างหาก
- Wild เขียนด้วย Rust และคาดหวังว่าจะสามารถจัดการความซับซ้อนของ incremental linking ด้วย Rust ได้
วิธีติดตั้ง
- สามารถดาวน์โหลด release tarball ได้จาก releases page จากนั้นแตกไฟล์แล้วคัดลอก binary
wild ไปยังตำแหน่งที่อยู่ใน PATH
- หากมี
cargo-binstall สามารถติดตั้งด้วยคำสั่งต่อไปนี้
cargo binstall wild-linker
- คำสั่งติดตั้งด้วย Homebrew มีดังนี้
brew install wild-linker/wild/wild
- หากต้องการ build release ล่าสุดจาก crates.io ให้ใช้คำสั่งต่อไปนี้
cargo install --locked wild-linker
- หากต้องการติดตั้งโค้ด unreleased ล่าสุดจาก Git ให้ใช้คำสั่งต่อไปนี้
cargo install --locked --bin wild --git https://github.com/wild-linker/wild.git wild-linker
- Wild เวอร์ชันเสถียรใน Nixpkgs สามารถใช้ผ่าน
pkgs.useWildLinker pkgs.stdenv ได้
- สำหรับ Git revision ล่าสุดที่ยังไม่เสถียร ให้ดูเอกสาร
nix/nix.md
วิธีใช้เป็นลิงเกอร์เริ่มต้น
- Wild ออกแบบให้เป็น drop-in replacement ซึ่งใช้ในลักษณะที่ GCC หรือ Clang เรียกใช้งานได้
- ตัวเลือกมีดังนี้
- ตัวเลือกเฉพาะ Clang
--ld-path=wild
-fuse-ld=wild ของ GCC 16.1 ขึ้นไปและ Clang
- Clang ต้องมี binary หรือ symbolic link ชื่อ
ld.wild
-B <path> ซึ่งรองรับทั่วไป
<path> คือไดเรกทอรีที่มี ld ซึ่งชี้ไปยัง wild
-
Rust และ Cargo
- ใน
~/.cargo/config.toml สามารถใช้ Clang ร่วมกับ --ld-path=wild ได้
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-Clink-arg=--ld-path=wild"]
- หรือสามารถใช้
-fuse-ld=wild ได้
[target.x86_64-unknown-linux-gnu]
# linker = "clang" # Uncomment this line if your GCC is older than version 16.
rustflags = ["-Clink-arg=-fuse-ld=wild"]
-
ระบบ build ของ C/C++
- ใน autotools, CMake, meson เป็นต้น โดยทั่วไปแค่ตั้งค่า
LDFLAGS ก็เพียงพอ
export LDFLAGS="${LDFLAGS} -fuse-ld=wild"
- ใน GCC เวอร์ชันเก่า สามารถสร้าง symbolic link
ld ที่ชี้ไปยัง wild แล้วส่งไดเรกทอรีนั้นผ่าน -B ได้
ln -s /usr/bin/wild /tmp/ld
export CFLAGS="${CFLAGS} -B/tmp"
export CXXFLAGS="${CXXFLAGS} -B/tmp"
export LDFLAGS="${LDFLAGS} -B/tmp"
- ระบบ build เหล่านี้อาจซับซ้อนได้ จึงแนะนำวิธีตรวจสอบด้วย
readelf ว่า Wild ถูกใช้ลิงก์ binary จริงหรือไม่
-
การตั้งค่า Cargo สำหรับ Illumos
- บน Illumos ต้องระบุ absolute path ของ Clang และต้องส่ง Wild เป็น absolute path ด้วย
- หากไม่ระบุ absolute path ของ Wild อาจถูก delegate ไปยัง GNU ld หรือ Sun ld อย่างเงียบ ๆ
[target.x86_64-unknown-illumos]
# Absolute path to clang - on OmniOS this is likely something like /opt/ooce/bin/clang.
linker = "/usr/bin/clang"
rustflags = [
# Will silently delegate to GNU ld or Sun ld unless the absolute path to Wild is provided.
"-Clink-arg=-fuse-ld=/absolute/path/to/wild"
]
สถานะการรองรับและรายการที่ยังไม่รองรับ
- แพลตฟอร์มและสถาปัตยกรรมที่รองรับในปัจจุบันมีดังนี้
-
x86-64 on Linux
-
ARM64 on Linux
-
RISC-V(riscv64gc) on Linux
- LoongArch64 on Linux
- อยู่ในสถานะรองรับระยะแรก
- PPC64LE on Linux
- อยู่ในสถานะรองรับระยะแรก
- ฟีเจอร์ที่ทำงานได้มีดังนี้ แต่มีหมายเหตุว่าอาจมีบั๊ก
- output เป็น binary แบบ static linked non-relocatable
- output เป็น position-independent binary แบบ static linked หรือ static-PIE
- output เป็น binary แบบ dynamic link
- output เป็นไฟล์ shared object
.so
- Rust proc-macro ที่ลิงก์ด้วย Wild
- ผ่านการทดสอบกับ crate ส่วนใหญ่ที่มียอดดาวน์โหลดสูงบน crates.io
- debug information
- รองรับ GNU jobserver
- รองรับ linker script บางส่วน
- ดูรายละเอียดได้ที่ linker script support matrix
- Linker plugin LTO
- มี known issues
- รายการใหญ่ที่ยังไม่ได้ implement มีดังนี้ โดยเรียงใกล้เคียงตามลำดับความสำคัญปัจจุบัน
-
incremental linking
- linker script ที่ซับซ้อนกว่า
- รองรับ Mach-O
- รองรับ Windows
ตรวจสอบว่าใช้ Wild หรือไม่
readelf สามารถติดตั้งได้จากแพ็กเกจ binutils และใช้คำสั่งต่อไปนี้เพื่อตรวจสอบสตริงใน section .comment
readelf --string-dump .comment my-executable
- หากมีบรรทัดลักษณะต่อไปนี้ แปลว่าเป็น binary ที่ลิงก์ด้วย Wild
Linker: Wild version 0.1.0
strings ก็ใช้งานได้จากแพ็กเกจ binutils เช่นกัน และสามารถตรวจสอบด้วยคำสั่งต่อไปนี้ได้ด้วย
strings my-executable | grep 'Linker:'
Benchmark และเป้าหมายการทดสอบ
- เป้าหมายของ Wild คือการเป็นลิงเกอร์ที่เร็วมากในที่สุดผ่าน incremental linking
- ยังมีเป้าหมายให้ non-incremental linking และ initial link เมื่อเปิดใช้ incremental linking ทำได้เร็วที่สุดเท่าที่เป็นไปได้
- benchmark ทั้งหมดรันภายใต้เงื่อนไขที่ output ไปยัง tmpfs
- วิธีรัน benchmark สรุปไว้ใน BENCHMARKING.md
- ระบบที่ใช้เป็นเป้าหมาย benchmark มีดังนี้
- ตัวอย่าง benchmark รวมถึงเวลา link และการใช้หน่วยความจำของ Chromium, เวลา link ของ
librustc-driver, เวลา link ของ Wild เอง, และเวลา link ของ rust-analyzer บน Raspberry Pi 5
ตัวอย่างการลิงก์โค้ด Rust และข้อมูลโปรเจกต์
- ตัวอย่างการใช้ Wild เพื่อ build และทดสอบ crate ใน
cargo test มีดังนี้
- คำสั่งนี้รันสำเร็จกับ crate ยอดนิยมบางตัว เช่น
ripgrep, serde, tokio, rand, bitflags
- binary
wild ต้องอยู่ใน PATH และเนื่องจาก GCC ไม่อนุญาตให้ใช้ลิงเกอร์ใดก็ได้ จึงจำเป็นต้องติดตั้ง Clang
RUSTFLAGS="-Clinker=clang -Clink-args=--ld-path=wild" cargo test
- หาก symbolic link
ld.wild ชี้ไปยัง wild ก็สามารถใช้วิธีต่อไปนี้ได้เช่นกัน
RUSTFLAGS="-Clinker=clang -Clink-args=-fuse-ld=wild" cargo test
- หากต้องการใช้ Wild เป็นลิงเกอร์สำหรับโค้ด Rust ใน CI ให้ดู wild-action
- ข้อมูลการมีส่วนร่วมอยู่ใน CONTRIBUTING.md และภาพรวมการออกแบบระดับสูงอยู่ใน DESIGN.md
- การสนทนาเกี่ยวกับ Wild ดำเนินอยู่บน Zulip server
- บล็อกของ David มีบทความจำนวนมากที่พูดถึงหลายแง่มุมของลิงเกอร์ Wild
- ใบอนุญาตเลือกได้ระหว่าง Apache License 2.0 หรือ MIT license
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
หลังจาก mold ถูก เปลี่ยนไลเซนส์จาก AGPL เป็น MIT (เป็นส่วนหนึ่งของรีลีส mold 2.0) ผมคิดว่าความจำเป็นที่จะต้องสร้างลิงเกอร์เร็ว ๆ อีกตัวทั่วโลกคงลดลงไปมากแล้ว เลยไม่คิดว่าจะมีโปรเจกต์แบบนี้ออกมา
แถมในบางกรณียัง เร็วกว่า mold 2 เท่า อยู่แล้ว ยิ่งคาดไม่ถึงเข้าไปอีก เลยตั้งใจจะคอยดูว่ามันจะพัฒนาไปอย่างไร และขอให้ผู้เขียนโชคดี
ลิงเกอร์ของ Microsoft มี incremental linking เป็นค่าเริ่มต้นมาตั้งหลายสิบปีแล้ว แต่บน Linux ยังไม่มี incremental linker ที่พร้อมใช้ในโปรดักชัน นี่ก็น่าอายอยู่เหมือนกัน
จาก issue บน GitHub ดูเหมือนผู้เขียนตอนแรกพยายามจะรองรับ แล้วก็ย้ายการรองรับ Windows ไปไว้ที่ลิงเกอร์ sold แต่ sold ก็ถูกเก็บเข้าคลังไปเมื่อไม่นานนี้ เลยงงว่าสุดท้ายคือไม่มีการรองรับ Windows หรือผมเข้าใจลำดับเรื่องผิดกันแน่
เคยลองดูมาก่อน แต่ยังไม่แน่ใจว่า พร้อมใช้ในโปรดักชัน แล้วหรือยัง
ถ้าดูจาก README เหมือนจะยังไม่พร้อม ผมเลยยังใช้ mold อยู่
ถ้าเป็นผู้ใช้ macOS ทาง Apple ออกลิงเกอร์ตัวใหม่เมื่อประมาณ 1–2 ปีก่อน และนั่นทำให้ผู้เขียน mold หยุดทำเวอร์ชัน macOS
ถ้าจะใช้กับ Rust ให้ใส่แบบนี้ใน
config.toml[target.aarch64-apple-darwin]rustflags = ["-C","link-arg=-fuse-ld=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld","-C","link-arg=-ld_new",]ผมติดตั้งเครื่องมือบรรทัดคำสั่งแล้ว แต่มีแค่
/usr/bin/ldกับ/usr/bin/ld-classicคงน่าสนใจถ้าคอมไพเลอร์ C/C++ ตัดขั้นกลางที่เรียกว่าการลิงก์ออกไป แล้ว build ทั้งโปรแกรมเป็นหน่วยเดียว
ถ้าคอมไพเลอร์มองเห็นทั้งโปรแกรมตั้งแต่แรก ก็ไม่จำเป็นต้องมี LTO เอง
เพื่อ incremental build ก็คงต้องเก็บผลลัพธ์บางส่วนของการ build ไว้ แต่ไม่ใช่ในรูป object file ต้องเป็น metadata ที่รู้ว่าค้ดที่สร้างมาจากที่ไหนและมี dependency อะไร เพื่อให้เปลี่ยนเฉพาะส่วนที่ถูกต้องได้
ทุกวันนี้ไลบรารีภายนอกส่วนใหญ่ลิงก์แบบไดนามิกอยู่แล้ว จึงไม่จำเป็นต้อง build จากซอร์ส และการเอาลิงเกอร์ออกก็ไม่ทำให้ dependency ที่ไม่เปิดเผยมีปัญหา
ถ้ายังไม่พอ ก็ให้คอมไพเลอร์รับ object file ได้ด้วย เพื่อจัดการกรณี legacy หรือกรณีพิเศษที่ต้องลิงก์แบบสแตติกเข้าไปในไบนารี
SQLite3 เอาทุกอย่างมาต่อกันเป็น หน่วยคอมไพล์เดียว
น่าจะมีคนใช้วิธีนี้อยู่แล้วมากกว่าที่คิด
https://sqlite.org/amalgamation.html
ความสนใจในลิงเกอร์ที่รวดเร็วเพิ่มขึ้นมากเพราะ การยอมรับและความนิยมของ Rust
แม้แต่ไบนารี Rust แบบ static link ขนาดพอประมาณ ก็อาจใช้เวลาหลายนาทีในขั้นตอนลิงก์ของการคอมไพล์โหมด release (เมื่อใช้ mold)
นี่ไม่ใช่ปัญหาเฉพาะของ Rust แต่โดยรวมเป็นผลจาก static link ที่เข้มงวด การปรับแต่งขั้นสูงในเวลาลิงก์อย่าง LTO และ BOLT ที่ LLVM ให้มา และเสียงบ่นเรื่องเวลา compile ในชุมชน Rust มารวมกัน
เพราะ Rust มีความสัมพันธ์แน่นแฟ้นกับ LLVM และแทบจะพึ่งพามัน จึงกลายเป็นภาษาที่นำเวทมนตร์ช่วงลิงก์ของ LLVM มาใช้แพร่หลายที่สุด ส่วน C++ ก็เจอปัญหาเดียวกันได้ แต่ตอนนั้นคนมักจะมองว่าเป็นความผิดของ toolchain มากกว่าของภาษา
ผมจับตาดู wild มาสักพักเพราะมีศักยภาพจะเป็น ลิงเกอร์แบบ incremental ที่ปรับแต่งประสิทธิภาพได้ แต่พูดตรง ๆ ก่อนที่มันจะทำ incremental linking ได้จริง ผมไม่มีแรงจูงใจจะลองจับเลย
อีกอย่าง ecosystem ของ C/C++ มีวัฒนธรรมยอมรับไลบรารีแบบไบนารี จึงมีหลายกรณีที่พอ clone repository หรือเปลี่ยน branch สำหรับพัฒนา ก็ไม่ต้องคอมไพล์ทั้งโลกใหม่ แค่คอมไพล์แอปพลิเคชันของตัวเองก็พอ
ถ้าให้ shell แอปภายนอกเรียก business logic ที่เป็น Wasm ก็ต้องคอมไพล์ใหม่แค่ logic ภายใน และ shell แอปภายนอกไม่ต้อง restart ด้วยซ้ำ
ปี 2008: ลิงเกอร์ใหม่ gold ที่ทำมาให้เร็วกว่า GNU ld
ราวปี 2015: ลิงเกอร์ทดแทนแบบ drop-in lld ที่เร็วกว่า gold อย่างน้อย 2 เท่า
ปี 2021: ลิงเกอร์ใหม่ mold ที่เร็วกว่า lld หลายเท่า
ปี 2025: ลิงเกอร์ใหม่ wild...
เรื่องที่เกี่ยวข้อง มีหนังสือเก่าแต่ดีของ John Levine ชื่อ Linkers and Loaders
เป็นเล่มสุดท้ายในรายการด้านล่าง
https://www.johnlevine.com/books.phtml
ผมอ่านเมื่อหลายปีก่อนและค่อนข้างน่าสนใจ ถือเป็นหนังสือมาตรฐานของด้านนี้
อย่างที่เห็นจากลิงก์ เขายังเขียนหนังสือคอมพิวเตอร์ยอดนิยมเล่มอื่น ๆ ด้วย
ดูมีอนาคตดีทีเดียว
เขียนด้วย Rust ตั้งแต่แรก และเป้าหมายคือให้เร็วพร้อมรองรับ incremental linking
ถ้าจะใช้กับ Rust ก็น่าจะใช้ gcc เป็น linker driver ได้เหมือนกัน
ใน
.cargo/config.tomlของโปรเจกต์:[target.x86_64-unknown-linux-gnu]rustflags = ["-C", "link-arg=-fuse-ld=wild"]นอกเรื่องนิดหนึ่ง แต่ทำไม Rust ถึงต้องไปผูกกับ gcc หรือ clang ตรงนี้ด้วย? มีฟีเจอร์อะไรที่ขาดไปหรือเปล่า?
น่าเสียดายที่ gcc ไม่รับลิงเกอร์ใด ๆ ผ่านแฟล็ก
-fuse-ld=ลิงเกอร์ที่รับมีแค่ bfd, gold, lld, mold เท่านั้น
ทำให้ gcc เรียก wild เป็นลิงเกอร์ได้ก็จริง แต่ตอนนี้ต้องสร้างไดเรกทอรีที่มีลิงเกอร์ wild อยู่ แล้วเปลี่ยนชื่อไบนารีหรือลิงก์สัญลักษณ์เป็น
"ld"จากนั้นส่ง-B/path/to/directory/containing/wildให้ gccเหตุผลที่ Rust ไม่เรียกลิงเกอร์โดยตรง แต่เรียกลิงเกอร์ผ่าน gcc หรือ clang คือคอมไพเลอร์ C รู้แฟล็กลิงเกอร์ที่จำเป็นสำหรับลิงก์กับ libc และ C runtime บนแพลตฟอร์มปัจจุบัน
เช่น
Scrt1.o,crti.o,crtbeginS.o,crtendS.o,crtn.oเป็นต้นเพราะคอมไพเลอร์ Rust สร้าง IR bytecode ไม่ใช่ภาษาเครื่อง
สงสัยว่า ในกรณีที่ไม่ใช่ incremental มีเหตุผลเชิงทฤษฎีอะไรที่ทำให้สิ่งนี้เร็วกว่า mold ได้?
“เพราะเป็น Rust” ใช้อธิบายได้ดีในหลายเรื่อง แต่ไม่ได้อธิบายข้อได้เปรียบด้านประสิทธิภาพที่คาดหวัง
ถ้าเป็น “เพราะมีโอกาสทำ parallel processing ที่ความยากต่ำ ซึ่ง Rust ช่วยให้ใช้ประโยชน์ได้ง่าย” ก็น่าสนใจ แต่ก็ไม่ได้ระบุหรือบอกเป็นนัยไว้เลย
เบาะแสอย่างหนึ่งคือเคยได้ยินว่า Mold จะเร็วขึ้นประมาณ 10% ถ้าใช้ allocator ที่เร็วกว่าอย่าง mimalloc
ผมลองใช้ mimalloc กับ Wild แล้ว แต่ไม่เห็นความเร็วเพิ่มขึ้นที่วัดได้
จากตรงนี้ดูเหมือนว่า Mold ใช้ allocator มากกว่า Wild และใน Wild ก็พยายามอย่างชัดเจนที่จะปรับลดจำนวนครั้งของ heap allocation
โดยรวมแล้วผมมองว่าเป็นความต่างของการตัดสินใจด้านการออกแบบ
ถ้าถามว่าสิ่งนี้เกี่ยวกับ Rust อย่างไร ผมมั่นใจว่าถ้าพอร์ต Wild จาก Rust ไปเป็น C หรือ C++ ประสิทธิภาพก็น่าจะเกือบเหมือนเดิม
เพียงแต่ใน Rust ด้วย borrow checker ทำให้แพตเทิร์นโค้ดที่โอเค อาจกลายเป็นกับดักใน C หรือ C++ และทำให้ดูแลรักษายาก
เมื่อก่อนตอนเขียน C++ ผมมักเขียนแบบ defensive มากกว่า แม้ต้องยอมเสียประสิทธิภาพเล็กน้อย แต่ใน Rust ผมรู้ว่าคอมไพเลอร์ช่วยดูให้ จึงเขียนได้กล้ากว่ามาก
บังเอิญจัง
เมื่อหนึ่งชั่วโมงก่อนผมเพิ่งเทียบประสิทธิภาพ wild, mold, ld ในโปรเจกต์ C ที่กำลังทำอยู่
มี 23,000 บรรทัด 172 ไฟล์ และ
gcc+ldใช้ user time ประมาณ 23.4 วินาที,gcc+mold22.5 วินาที,gcc+wild21.8 วินาทีดังนั้นผมคิดว่าถ้าเป็นโปรเจกต์ที่มีโครงสร้างดี เวลา link อาจไม่ใช่ปัญหาใหญ่นัก
ในกรณีนั้นเวลาส่วนใหญ่ใช้ไปกับการ compile โค้ด ไม่ใช่ linking
ข้อดีของลิงเกอร์เร็วจะเห็นชัดที่สุดใน iterative development
กล่าวคือหลังจากแก้โค้ดเล็กน้อยแล้ว build ใหม่และรันผลลัพธ์ คอมไพเลอร์มักแทบไม่มีอะไรต้องทำ แต่ linking ยังต้องทำใหม่ตั้งแต่ต้น จึงมักกลายเป็นเวลาหลัก
ld.lldล่ะ?“เบนช์มาร์กนี้รันบนแล็ปท็อปของ David Lattimore (System76 Lemur Pro รุ่นปี 2020) ซึ่งมี 4 คอร์ (8 เธรด) และ RAM 42GB”
https://news.ycombinator.com/item?id=33330499
ขอเสริมว่าไม่ได้จะบอกว่า wild บวมเกินไป
ถ้ามีปัญหา ปัญหาน่าจะอยู่ที่ซอฟต์แวร์ที่ถูกพัฒนาด้วยสิ่งนั้น และคอมพิวเตอร์ของคนที่จะใช้ซอฟต์แวร์เหล่านั้นมากกว่า
https://news.ycombinator.com/item?id=42896619
“... RAM มี 16GB และอัปเกรดไม่ได้...”
พูดกึ่งล้อเล่นนะ แต่ถ้าคนเขียนโค้ด Rust ก็น่าจะมี RAM 32GB อยู่แล้ว
ส่วนตัวแล้ว การอัปเกรดแล็ปท็อปเป็น 64GB โดยยอมเสียสละทุกอย่างอื่นอย่างแท้จริง แทบจะเป็นการตัดสินใจที่ยอดเยี่ยม
ที่บอกว่าแทบจะ เพราะผมน่าจะไม่ทุ่มทั้งหมดไปกับ RAM แต่ควรเอาเงินไปลงทั้ง RAM และจอด้วย
ข้อเสียเดียวคือการจัดการแท็บที่เปิดไว้สัปดาห์ละครั้ง กลายเป็นงานที่กินเวลาทั้งเย็นไปแล้ว