昨天把時間軸講完了,今天換一件更俗、也更關鍵的事——要有圖要讓人看得懂 R
通常資深的 APR 工程師心裡會有一張 flow 的流程圖,但對生為菜鳥而言的我眼前卻只有一坨 script
WinFlow 把 Generator 跟 Runner 都做成「同一張圖的兩種讀法」:一邊拿來改依賴,一邊拿來看狀態。畫布不是裝飾,是 flow.json 的投影。
好的流程圖要讓沒寫過這條 flow 的人,十秒內知道:誰等誰、誰正在跑、點進去能看到什麼。
本篇焦點放在 DAG 怎麼從 parents / children 長到 Tkinter canvas 上。GUI 的按鈕列會帶到,但不展開成操作手冊。
winflow_gui 的視窗裡有兩個分頁:
+------------------+ Sync from Generator +------------------+
| Generator | -------------------------> | Runner |
| 編輯 DAG | flow.json | 監看 DAG |
| 問:誰該等誰? | | 問:現在跑到哪? |
+------------------+ +------------------+
\ /
\ /
+----------------------------------+
| winflow/graph.py |
| build_relation_edges |
| compute_layers |
+----------------------------------+
Generator 的 FlowEditorCanvas 與 Runner 的 FlowGraphCanvas 都吃同一套作法。差在投影:
| Generator | Runner | |
|---|---|---|
| 問題 | 依賴對不對、能不能改 | 現在哪個 job 在 PEND / RUN / DONE |
| 節點顏色 | 選取關係(自己、父、子) | LSF 狀態 |
| 邊 | 正交折線,中間可標檔名 | 直線箭頭,不標檔名 |
| 位置 | 可拖、可 Auto Layout,寫進 document | 每次 redraw 依層重排,不能拖 |
| 點一下 | Highlight 父與子;連線模式用來 Link / Unlink job | 開 Job 詳情:I/O、時間、單跑 / 停 / 驗檔 |
讓「菜鳥一眼看懂」不是靠兩套不同的圖,而是同一張 DAG,用顏色回答當下那個分頁在問的問題。
共享模組 winflow/graph.py 裡,真正拿來畫的 function 是 build_relation_edges():
parents(存的永遠是 stage/task/job 這種 slash key)input 剛好等於 parent 的某個 output,標籤用那個檔名;否則標成 (parent),表示這是純關係邊、沒有共享檔JSON 裡的 children 是互逆欄位,用來給 Runner 解鎖下游,不另外再長一套邊。畫的時候認 parents 就夠,避免同一對節點被畫兩次。
舊檔若缺 parents / children,ensure_job_relations() 會在載入進畫布之前用 task 順序 + 檔案路徑補一次。補完之後,畫布就只認屬性了。Generator 上 Unlink 掉的關係,不會因為「看起來同一 task 前後相鄰」又自己長回來
Generator 內部的 job key 用 NUL 字元隔開(stage\0task\0job),避免 job 名稱裡剛好也有 /。寫進 flow.json 時再轉回 slash。我們不必管這個;只要記得:畫面上的一條箭頭,對應 JSON 裡的一組 parents/children,不是對應目錄排列。
有依賴關係之後要決定節點站在哪一列。兩邊共用 compute_layers(),做法是拓樸分層:
1. 每個節點 indegree = 有幾條邊指進來
2. indegree = 0 的放進 queue,layer = 0(畫面上寫 inputs)
3. 每處理完一個點,把它的下游 indegree 減一
4. 下游的層號 = max(自己目前的層, 上游層 + 1)
5. 減到 0 的人進入下一輪
結果很直觀:
layer 0 layer 1 layer 2
(inputs) (step 1) (step 2)
IT_1 ----+-----> IT_3 ----+-----> IT_4 -----> IT_5
| |
IT_2_0 --+ |
IT_2_1 --+----------------+
IT_2_2 --+
同一層的人沒有互相等待,理論上可以並行。這就是為什麼 QC 會跟下一主站並排,而不是畫在主站正下方當「下一個 stage」。層是依賴深度,不是公司組織圖
若圖裡不小心有連成 circle 的話,就會造成某些點的 indegree 永遠減不完。Generator 的 Link 因此會先跑 would_create_cycle():從「即將當 child 的那顆」沿 children 往下走,若走得到「即將當 parent 的那顆」,這條連線就會直接被拒絕。可視化建立在「這張圖是 DAG」這個前提上;環一出現,層與排程都會一起壞。
Auto Layout 的功能只是把層號變成座標。Generator 的 layout_by_graph():
document.positions
Runner 更乾脆:每次 redraw 都重算,x 間距 80、y 間距 28,使用者不能拖這些 job node
Generator 才會提供你這個功能,好讓你思索怎麼串接自己的 flow

打開 Generator,右邊是 Flow canvas,左邊是 Flow settings 與 job 清單。工具列上跟圖最有關的是:
flow.json 進畫布提示列寫著:Drag to rearrange | Double-click to edit | Esc cancels link。拖動只改座標,不會改 job 在 JSON 陣列裡的順序,也不會改依賴。這是刻意的:排版是給人看的。
固定會三行:
+------------------------------+
| | FLOOR_PLAN | ← job name(太長會截成 ...)
| | FLOOR_PLAN | normal | ← task | queue | 可選 -m host
| | 1 CPU | ^0 v2 | ← CPU,以及幾個父、幾個子
+------------------------------+
^0 v2 讀作:沒有父親、兩個孩子。沒有任何邊的孤立點則顯示 no deps。滑鼠移上去會出 tooltip,補上完整的 stage / task、machine、以及 command——畫布不把指令印在卡片上,否則十個 job 就會變成十張便利貼小說。
顏色不表示跑到哪(Generator 還沒送 LSF),而表示你現在在看誰的親戚:
| 角色 | 填色 | 左邊色帶 | 意思 |
|---|---|---|---|
| 普通 | 白 | 藍 | 沒被選到 |
| 選中 | 淡藍 | 藍、邊框加粗 | 你點的這顆 |
| 它的 parent | 淡綠 | 綠 | 它在等的人 |
| 它的 child | 淡黃 | 琥珀 | 等它的人 |
| Link 起點 | 淡橘 | 橘 | 連線模式的第一下 |
點 PLACE,FloorPlan 變綠、Q_PLACE 與 CTS 變黃——菜鳥不必去數 JSON 裡的 key,一眼就知道這顆的上下游。沒被選到的邊會淡掉,選中相關的邊變藍、加粗
邊是正交折線,不是斜線:
[ IT_1 ]——┐
│
└──────►[ IT_3 ]
IT_1.done
從父節點右緣中點出發,水平走到兩點中線,垂直對齊子節點,再水平進子節點左緣,箭頭在終點。折線中間會寫標籤,超過 22 字截斷。標籤通常是共享的檔名(example_flow/out/IT_1.done);若只是 Link 出來、沒有檔案依賴,就會看到 (parent)。
這讓「圖上的邊」跟「磁碟上的檔」可以同時存在、又不必混成一件事。QC 吃的 .done,邊上會寫那個路徑;你若只想說「這個 Summary 要等四個 QC」,但沒有檔按依賴,這個關係仍然在,只是標籤就是 parent
背景還有兩層給人定位、不參與排程的東西:
inputs,其後寫 step 1、step 2。告訴你「這一列是入口還是已經等過一輪」。按 Link ->,游標變十字。第一下選父(橘),第二下選子。link_jobs() 會:
Unlink 反向清邊,並盡量拿掉不再被引用的 dummy。畫布立刻 redraw()。你在 GUI 上掰開的那條線,就是之後 Export 進 Runner 的那條線。

來看一下我們的 ithome flow
切到 Runner,或按 Sync from Generator(僅在沒有 job 在跑、也沒有正在 kill 時可按)。畫布換成監看模式:節點變小(130×58),不能拖,每次重畫都依層站好。
左邊控制列是 Run Flow / Rerun / Stop / Reset;中間偏上是 DAG,偏下是 log。DAG 回答「現在誰在動」,log 回答「那個人嘴裡在喊什麼」。本篇只盯 DAG。
+------------------+
| IT_1 | ← 名稱;過長會在 _ 處折行,再不行就縮小字再截斷
| PEND | ← 狀態
+------------------+
卡片底色就是狀態。這是 Runner 可視化的全部賭注:顏色 = 生命週期
| 狀態 | 顏色 | 菜鳥該讀成 |
|---|---|---|
| waiting / pending | 灰 | 還沒輪到;父還沒全 DONE |
| PEND | 淺天藍,深色字 | 已 bsub,在 LSF 排隊 |
| RUN | 藍,白字 | 機器上正在跑 |
| DONE | 綠 | LSF 說完,且 outputs 都在 |
| EXIT / failed | 紅 | 掛了,或缺檔被判失敗 |
| KILLING | 紫 | Stop 已送 bkill,還在確認 |
| UNKNOWN | 灰字 | bjobs 回了認不得的字 (SSUSP 這類的狀態碼) |
PEND 特別用淺底深字,避免一整排排隊 job 變成刺眼色塊。其餘有顏色的狀態用白字,讓 DONE 的綠、EXIT 的紅在十公尺外也分得出來。
PEND、RUN、KILLING 會被放進 active_keys。畫布每 600ms 在節點外再畫一圈橘紅(#fd8c73),像是心跳那樣。並行時,同一層可以同時有好幾顆在閃——那不是動畫炫技,是在說:這些 job 已經通過依賴檢查,正在跟 LSF 說話。
DONE / EXIT 會從 active 集合拿掉,心跳停。Reset Flow 把所有節點打回 waiting,並清掉 lsf_name、job_id、起迄時間;它不 bkill。Stop 才是殺 cluster 上的作業,對應的節點會先變紫再變紅。
邊在 Runner 上較克制:父右緣到子左緣的直線箭頭,灰色,沒有檔名。監看時你要的是「誰解鎖誰」,不是再讀一次路徑。檔名改去點節點。
頂部仍有 stage 斜體小字,對齊該 stage 第一個出現的層。它只是名牌。QC 若跟下一主站同層,stage 標籤可能重疊在同一直欄——這時候請相信節點顏色與邊,不要相信標籤的左右順序。
點節點會開 JobDetailDialog,這才是「內容」的主場。畫布只留名字與狀態,詳情視窗分三塊:
(none)
時間從 Runner 收到的 callback 記入。job_start / job_submitted 寫 start;job_done / job_failed 寫 end。畫布上的綠與紅,背後是這段時間戳記。
Validate 不送 LSF,只問 outputs 是否已在磁碟;都在就把這顆標成 DONE。這是在「檔案已經生出來、但 GUI 還停在 wait」的作弊法 (實務上很常會遇到),不是另一套排程。Run Job 則是單點重送,整條 flow 在跑或正在 kill 時會禁用,避免跟 DAG 排程搶人。
狀態怎麼從 LSF 流到 GUI 上的呢 ?
FlowRunner.run_job
→ job_callback(job_start / submitted / status / done / failed)
→ GUI 執行緒 _handle_job_event
→ node.status / lsf_name / job_id / 時間
→ graph_canvas.update_job(...)
→ 改色、更新 active_keys、redraw
job_status 帶的是 bjobs 的 PEND / RUN / DONE / EXIT。GUI 不自己猜。輪詢時只問 LSF,畫布只負責翻譯成顏色。
在 Generator
inputs:沒有箭頭指進來的人,是可以一起開工的 Root(parent) 表示純順序^n vm:數字跟你心裡的上下游人數不一樣,就去查 Link 是不是多點或少點在 Runner
APR + QC 那張經典圖,用這套讀法會變成:
FLOOR_PLAN (layer 0)
├─► Q_FLOOR_PLAN (layer 1) ← 旁掛,不擋住 Place 右邊的人
└─► PLACE (layer 1)
├─► Q_PLACE (layer 2)
└─► CTS (layer 2)
QC 跟下一主站站在同一層,所以顏色會一起從灰變藍。 若你公司規定 QC 必須擋住主鏈,Generator 裡就該把 QC 接到下一主站的 parents;層數會加一,Runner 上也會變成「主 flow 先一直灰,直到 QC 變綠」。
有人會問:為什麼不在 Generator 上也畫狀態,或在 Runner 上也能拖節點、改邊?
因為它們回答的時間點不同。編輯時改邊,執行中的狀態就作廢;執行中拖節點,只會讓「誰在跑」跟「誰看起來站在誰旁邊」脫鉤。WinFlow 選擇:
flow.json 換手,而且換手當下 Runner 必須閒置build_relation_edges 與 compute_layers:邊來自 parents,層來自 flow 深度^父 v子,邊上可標檔名