[Android] มาทำความรู้จักกับโปรเจค Android ของเราให้มากขึ้นดีกว่า (part 2)
การทำระบบ build tracking เพื่อ monitor โปรเจคของเรา
สืบเนื่องจากพาร์ทที่แล้ว นอกจากการไล่เช็ค dependency เพื่อ Modularization ที่ดีแล้ว เรายังต้องหาต่อด้วยว่า bottleneck ของการ build โปรเจคเรามันมาจากไหนกัน
ซึ่ง การที่จะรู้ได้ว่า bottleneck มาจากไหน เราต้องทำความเข้าใจกันก่อนว่า Gradle นั้น build โปรเจคเราแบบไหน เราถึงจะค่อย measure ทีหลังได้ว่าส่วนต่างๆใช้ระยะเวลาเท่าไหร่
Gradle tasks dependency
นอกจากโปรเจคเราจะมีระบบ dependency graph แล้ว ตัว Gradle เองก็มีการทำงานด้วยระบบ dependency graph เช่นกัน เวลาเราเรียกใช้ Gradle task หนึ่งที ตัว Gradle เองก็จะมีการคิดว่า task นั้นสามารถรันได้เลยรึเปล่า มี task อื่นๆที่เป็น dependency ด้วยหรือไม่ จากนั้น tasks graph ก็จะถูกสร้างขึ้นมาแล้วก็ส่งต่อให้ gradle daemon เป็นคนจัดการต่อ
ยกตัวอย่างเช่น ถ้าผมลองสั่ง build แอปเปล่าๆขึ้นมาด้วย :app:assembleDebug
ดู จะมีอะไรเกิดขึ้นบ้าง (ภาพข้างล่างใช้ gradle-taskinfo ในการสร้าง tree ขึ้นมานะครับ ลองเล่นกันดูได้)
จะเห็นว่า assembleDebug
เองก็มี dependency อยู่หลาย task เช่นกัน ได้แก่ compileDebugSources
, mergeDebugNativeDebugMetadata
และ packageDebug
ซึ่งแต่ละอัน ก็มี dependency ซ้อนลงไปอีก!!
และถ้าเราลองดูลำดับการ execute gradle task แต่ละอันก็จะเป็นดังนี้
ซึ่งการที่เรารู้ลำดับในการ execute Gradle tasks เนี่ย มันจะช่วยให้เราเข้าใจมากขึ้นว่าในแต่ละครั้งที่เรา build แอป หรือเรียก Gradle task ใดๆก็ตาม มันถูกเรียกใช้ยังไง และ เหนือไปกว่านั้น ถ้าเราสามารถรู้ระยะเวลาที่ถูกใช้ในแต่ละ task เราก็จะเริ่มมองเห็นแล้วว่า bottleneck นั้นอยู่ที่ไหน จากนั้นก็ค่อยๆแก้ไปทีละจุด
ณ ตอนนี้ เรารู้แล้วว่ากว่าที่เราจะได้แอปขึ้นมาหนึ่งอัน เพื่อ run ในเครื่องโทรศัพท์ของเรา Gradle นั้นทำอะไรกับโปรเจคของเราบ้าง สิ่งสุดท้ายที่เหลืออยู่ก็แค่การวัดระยะเวลาของแต่ละ task แล้วว่าเป็นอย่างไร เพราะเมื่อเรามี Measurement เราก็จะสามารถวิเคราะห์ได้โดยมี data ที่รองรับสมมติฐานในการแก้ปัญหานั่นเอง (Measurement เป็นอะไรที่สำคัญมากจริงๆนะ)
Gradle task tracking
โดยปกติแล้ว Gradle จะบอกอยู่แล้วว่าแต่ละ task ใช้เวลาเท่าไหร่ แต่อาจจะขาดๆไปบ้าง ซึ่งถ้าถามว่าเพียงพอต่อการนำไปใช้งานหรือไม่ คำตอบก็คือ ได้เหมือนกันครับ แต่ไม่ scalable อย่างมากๆ เพราะถ้าเรามีจำนวน task หลักร้อยหรือหลักพัน ก็คงยากที่จะมาไล่ดูทีละอัน และถ้ามีเครื่องคอมของ developer ซัก 10คน 100คน ก็จะยากที่จะเก็บข้อมูลเหล่านี้ออกมาอีก
วันนี้ผมเลยมีตัวช่วยสำคัญ ที่เราจะเอามาใช้ในการ Measure ในส่วนของ Gradle task มาแนะนำกันน เรียกได้ว่าใช้ได้ตั้งแต่ scale 1 คน จนเป็นทั้งองค์กร ก็ยังไหว
Introducing Talaiot
ซึ่งตัวช่วยก็คือ Talaiot นั่นเองง เป็น Gradle plugin จากคุณ Iñaki Villar ที่เรียกว่าเป็นคนจุดประกายให้ผมหันมาสนใจเรื่อง Build system ของแอนดรอยด์เลยก็ว่าได้
ซึ่งเจ้า plugin นี้หลักๆจะทำหน้าที่เป็น Listener ที่เก็บข้อมูลจากตัว Lifecycle ของ Gradle และทำหน้าที่เป็น Publisher ที่คอยส่งข้อมูลให้เรานำไปเก็บและเอามาใช้ได้ในภายหลัง
ซึ่งวิธีการใช้งานก็ง่ายมากๆ ตามขั้นตอนใน Github ได้เลย ขึ้นอยู่กับโปรเจคของเราว่า setup แบบไหนไว้อยู่ โดยในตัวอย่างนี้ผมจะใช้กับ gradle config แบบปกติที่ได้มาจากการสร้างโปรเจคใหม่เลย ในที่นี้ผมจะใช้ควบคู่กับ InfluxDB เป็นที่เก็บข้อมูลอีกที
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.cdsap.talaiot' version '1.4.2'
}//build.gradle ที่โมดูล app
จากนั้นก็ทำการ config ตัว plugin ได้เลย
talaiot {
publishers {
influxDbPublisher {
dbName = "tracking"
url = "http://localhost:8086"
}
}
}//build.gradle ที่โมดูล app
โดยที่ Publisher จะขึ้นอยู่กับตัว Consumer ที่ใช้ เพียงแค่ตั้งค่าให้ถูกต้องเท่านั้นก็ใช้ได้เลย ซึ่งในที่นี้ผมมี influxDB อยู่ในเครื่องอยู่แล้ว ก็สามารถชี้ไปยัง url ของ InfluxDB ได้เลย
จริงๆแล้วเราจะใช้ตัว Consumer เป็นอะไรก็ได้เช่นกัน ที่ผมใช้ InfluxDB เพราะรู้สึกว่าคล่องมือมากกว่า แถม setup ก็ง่าย 😂 จากนั้นเราก็ลอง build แอปดูได้เลย หรือรัน gradle task ไหนๆก็ได้เช่นกัน
แต่ข้อมูลที่เก็บมาเนี่ย มันค่อนข้างจะเป็นอะไรที่ดูยากพอสมควรเลย ทางที่ดี เราควรจะหา Visualization tool ที่เหมาะสมด้วยเช่นกัน ซึ่งก็คงจะหนีไม่พ้นใครนอกจาก Grafana (จริงๆ Chronograf ก็ใช้ได้นะ แต่ Grafana จะมีความ flexible มากกว่าในด้านของ datasource ที่รองรับ)
ซึ่งถ้าใครที่ setup ทุกอย่างไว้อยู่แล้วตั้งแต่แรก ใน Repo ของ Talaiot เองก็มี dashboard มาให้แล้วเช่นกัน ไม่ต้องมาสร้างเองง สามารถ import เข้า grafana ได้เลย
https://github.com/cdsap/Talaiot/blob/master/docker/grafana/dashboards/talaiot.json
แต่!!! ถ้าใครยังไม่มีทั้ง InfluxDB และ Grafana ลงไว้ในเครื่อง คุณ Iñaki ก็ได้เตรียม Docker image ไว้ให้ด้วยเหมือนกัน สามารถเข้าไปดาวน์โหลดได้จาก Repo นี้เลย
docker run -d \
-p 3003:3003 \
-p 3004:8083 \
-p 8086:8086 \
-p 22022:22 \
-v /var/lib/influxdb \
-v /var/lib/grafana \
cdsap/talaiot:latest
เราก็จะได้ทั้ง InfluxDB และ Grafana ที่มี pre-imported dashboards ให้เรียบร้อยแล้วว
นอกจากนั้น เรายังสามารถปรับตัว Talaiot ให้ส่งข้อมูลเฉพาะบางอย่างที่เราสนใจก็ได้นะ เพียงแค่เพิ่ม option ต่างๆใน config ที่ gradle extension ได้เลย
ไม่ว่าจะเป็นการ opt-out การ tracking จาก CI Machine เพราะบางทีเราอาจจะต้องการเก็บแค่จาก developer machine อย่างเดียว ก็สามารถระบุไปได้เลย ตามนี้
talaiot {
ignoreWhen {
envName = "CI" //แล้วบน CI เราก็ inject env variable เข้ามา
envValue = "true"
}
}
ซึ่งสามารถดูใน Documentation ได้เลย เค้าเขียนไว้ละเอียดมากก
จากการเก็บข้อมูล Gradle tasks ที่ผ่านมาและการ Visualization นั้นบอกอะไรเราบ้างล่ะ?
เมื่อเรามี measurement ที่ดีแล้ว คราวนี้เราก็จะมีข้อมูลซัพพอร์ทสมมติฐานต่างๆได้แล้ว ว่าทำไมโปรเจคของเราถึง build ได้ช้า มีส่วนไหนที่เป็นปัญหาบ้าง ยกตัวอย่างเช่น ผมมี Machine อยู่ 2 เครื่อง ทำการสั่ง build เหมือนๆกัน หลายๆครั้ง ก็จะเห็นว่าได้ค่าที่แตกต่างกันตามตารางด้านล่าง
จะพอเห็นได้ว่า มีปัจจัยอะไรบ้างที่แตกต่างกันระหว่างสองเครื่องนี้
- Gradle version
- Java VM Name
- Username….(ไม่นับ!!)
ซึ่งเรายังบอกไม่ได้เช่นกันว่าอะไรทำให้แตกต่างจริงๆ งั้นลองมาดูแบบเทียบ 1–1 ให้มากขึ้นซิ
สองอันนี้คือการสั่ง assemble
ด้วยสถานะที่ cache ทุกอย่างเรียบร้อยแล้ว (ก็คือแทบไม่มีการ re-compile code เลยเพราะโค้ดไม่เปลี่ยน)
ซึ่งเครื่องบนคือ Macbook Pro ปี 2016 ที่ใช้ JDK เวอร์ชั่น 1.8 จาก OpenJDK บน Gradle 6.8.1 และในทางกลับกัน เครื่องล่างคือ Mac Mini M1 ปี 2020 ที่ใช้ Zulu JDK เวอร์ชั่น 11 บน Gradle 6.8.2
ก็ยังบอกไม่ได้อีกว่าอะไรดีกว่ากัน!!
นั่นเป็นเพราะเราไม่มี control variable ในการทดสอบนั่นเอง ซึ่งผมเองก็แอบขี้เกียจในการ set environment ให้เหมือนๆกันด้วยแหละ 🥺 แต่คิดว่าทุกคนที่กำลังอ่านอยู่น่าจะพอเห็นภาพเหมือนกันแล้วใช่มั้ยย ว่าเราสามารถนำข้อมูลเหล่านี้ไปวิเคราะห์ต่อยอดอย่างไรได้บ้าง 😆
ลองดูแบบมี control variable ละกัน
ลองเปลี่ยนแค่ Gradle Version จาก 6.8.1 เป็น 6.8.2 ดู…ถามว่าสามารถสรุปได้รึเปล่าว่ามันเร็วขึ้น เมื่อเทียบจากข้อมูลคู่นี้
ไม่เสมอไป!! อ่าว….
เหตุผลที่ไม่สามารถสรุปได้ทันทีว่า Gradle 6.8.2 นั้นเร็วกว่า 6.8.1 อยู่ 0.14 วินาที เพราะเราทำการเทียบแค่ครั้งเดียวครับ ซึ่งถ้าสลับกันรันไปเรื่อยๆ 100 ครั้ง เราอาจจะได้ผลที่แตกต่างกันก็เป็นได้ ขึ้นอยู่กับสภาพของ Machine ณ ตอนนั้นด้วย
ซึ่งตามหลักสถิติแล้ว เราควรจะเทียบในหลายๆ percentile ด้วย โดยอันนี้จะอยู่ที่เราและ ว่าอยากดูที่ระดับไหนบ้าง อาจจะเช็คคร่าวๆที่ P50, P90 (ถ้าอยากดีเทลมากๆก็ P95) ไปเลย ซึ่งจำนวนข้อมูลที่เรามีก็ต้องสอดคล้องเช่นกัน อาจจะต้องมา weight กับขนาดของทีมอีกทีนึง
พอจะเห็นภาพกันมากขึ้นแล้วใช่มั้ยครับ ว่าเราสามารถนำ Measurement + Visualization มาวิเคราะห์โปรเจคเราอย่างไรได้บ้าง
ซึ่งด้านบนที่กล่าวมาเนี่ย เป็นแค่การดู Metric Build อย่างเดียวจาก Talaiot ซึ่งจะบอกเราเกี่ยวกับข้อมูลของการ build ในแต่ละครั้ง ไม่ว่าจะเป็น HW, Gradle Version, JVM Version, Configuration ต่างๆ ซึ่งยังมีอีกหลายอย่างให้นำมาดูได้ สามารถดูได้จากใน Repo ของ Talaiot เลยครับ วาร์ป
ลองมาดูอีก Metric นึงกันดีกว่า
สมมติว่าผมมีโจทย์ที่ต้องการแก้อันนึง ก็คือ “แอปเราใช้เวลา assemble
เท่าไหร่” โจทย์ง่ายๆเลย ผมสามารถนำข้อมูลที่มี + Grafana มาสร้าง dashboard ยังไงได้บ้างนะ
Step 1 : สร้าง Panel Table มาก่อนเลยอันนึง
จากนั้นผมลอง query จาก measurement task ใน rpTalaiot ดู (rpTalaiot คือ default retention policy ของ InfluxDB publisher ที่ใช้ ลองศึกษาเพิ่มในส่วนของ InfluxDB ได้ที่นี่ครับ วาร์ป)
โดยผมจะเลือกเฉพาะ task :app:assemble
เท่านั้น โดยเอาเฉพาะ mean มาดู
โดย default แล้ว unit ของ task แต่ละอันจะไม่ใช่หน่วยวินาทีครับ แต่เป็นหน่วย millisecond (ms) เพราะฉะนั้นเราต้องมา format กันที่หน้างานซะหน่อย ผ่าน Column Style ของ Grafana
ออกมาดูเป็นผู้เป็นคนขึ้นเยอะ!! แต่เนื่องจากเราไม่ได้ group ว่าจะ group by อะไร ทำให้เราเห็นเป็นค่า mean ของ :app:assemble
ทั้งหมดนั่นเอง แปลว่าเราควรใช้ panel แบบอื่นที่ดีกว่านี้!! ซึ่งในเคสนี้ใช้ Singlestat น่าจะเหมาะกว่า โดยเลือก threshold ตามขนาดโปรเจคเราได้เลย (ค่อยๆจูนกันไปก็ได้ระหว่างเก็บข้อมูล)
ลองเพิ่มอีกอันนึงเพื่อดูค่าเฉลี่ยของ task ที่สำคัญๆละกัน เช่น assemble
, build
, compileDebugKotlin
และอื่นๆตามใจชอบเลยย คราวนี้เราก็จะรู้แล้วว่า task ไหนนั้นกินเวลานาน หรือมาจาก module ไหนกันที่ compile นานกว่าเพื่อนเค้า เราก็จะแก้ปัญหาเป็นจุดๆได้ (อาจจะแตกโมดูลเพิ่ม หรือ optimize อื่นๆก็ว่ากันไปตามเคส)
ซึ่งพอได้ 2 panel ที่รู้สึกพอใจแล้ว ก็ทำการเซฟได้เลย Grafana ก็จะเก็บ panel เหล่านี้ไว้ใน dashboard ให้ (หรือจะ export ไปให้คนอื่น หรือเก็บไว้ที่อื่นเป็น JSON ก็ได้นะ)
ถ้าใครมี 2 panel นี้แล้ว แนะนำให้ลองเล่นกับ Talaiot และ Grafana ดูเลยครับ จะเห็นได้เลยว่ามันช่วยให้เราเห็นภาพปัญหาต่างๆของโปรเจคเราได้จริงๆ หรือถ้าเราอยากโชว์ข้อมูลอะไรออกมา ก็ visualize ได้ง่ายๆเลย (ยิ่งมีข้อมูลในทีมเยอะ ยิ่งรู้ปัญหาของโปรเจคเราได้ดีมากขึ้น!)
โค้ดของ dashboard ผมที่เพิ่มขึ้นมาอีก 2 panel ก็จะเป็นประมาณนี้ (วาร์ป)
มาถึงตรงนี้แล้ว หลังจากที่เราเข้าใจทั้งการ Modularization ของโปรเจคเราแล้ว และวิธีการทำงานของเจ้าตัว Gradle เองแล้ว พอเราหา bottleneck ที่เกิดขึ้นได้ เราก็จะเริ่มลด scope ของสิ่งที่ต้องแก้ให้กับโปรเจคเรา แต่ถ้ามันยังไม่สุดล่ะ ยังรู้สึกว่ายังเร็วกว่านี้ได้อีก!! ทีนี้เราต้องมาว่ากันด้วยเรื่องของการ Optimization แล้วล่ะ ว่าจะรีดความสามารถของ Machine และ Build system ได้ยังไง ซึ่งติดตามกันได้ในตอนต่อไปปป~~~
มาทำความรู้จักกับโปรเจค Android ของเราให้มากขึ้นดีกว่า (part 1)
มาทำความรู้จักกับโปรเจค Android ของเราให้มากขึ้นดีกว่า (part 2) (อันนี้)
มาทำความรู้จักกับโปรเจค Android ของเราให้มากขึ้นดีกว่า (part 3) (เร็วๆนี้)