2016年12月31日 星期六

如何在Pull request 被merge/revert 後再送 pull request

自從換了智慧型手機,又搭配1.5G的網路之後,用手機上網的機會大增,在用Firefox 刷Facebook 的時候,很容易跑出廣告來,於是發想把「在Yahoo 台灣大殺四方驚動萬教的人生溫拿勝利組強者我同學 qcl 大神」所寫的神專案QClean 移植到Firefox Mobile 上面。

這部分後來還在努力中,目標是把QClean 的Add-On 移植到Mobile上,研究時,Mozilla 的網頁都跑出一直跑出提醒:Add-on 寫的 plugin 很快就會被淘汰,請改用 Web extension 來寫,想說就順便,在寫mobile version 前,先把本來用 add-on 寫的QClean for Firefox 搬移到 Web-extension 上面。
總體來說是沒什麼難處,把原本用 Web-extension 寫的QClean for Google Chrome,複製一份,然後把裡面web extension 的API 從 chrome 取代為 Mozilla 所用的 browser ,結果就差不多會動了,真的是超級狂,整體來說沒什麼工作。

到時再送出Pull Request 之後,發現寫的Makefile 裡面有typo,雖然緊急在Pull Request 下面留言<先不要merge>,但在作者看到之前已經被merge 了。
接下來出現一個很有趣的狀況,直接幫我把 typo 修好並commit 之後,對上游的master branch送出新的 Pull request,github 卻顯示這個 Pull Request 有衝突,沒辦法像之前一樣直接合併。

後來想了一下才發現問題在哪裡,目前repository 的狀況大概像這個樣子。


問題在於送出新的 Pull Request 的時候,github 會比較兩個 branch 最近的共同祖先,從那個 commit 開始算pull request,在上圖中會是那個”Add features 2” 的commit。
這時我們從”fix some typo” 送了PR,github 會比較這個新的commit ,跟revert 過後的commit 作比較,因為revert 這個commit 修改的檔案(等同我們Pull Request的修改只是剛好反向),兩者因此有衝突;同時,之前的 commit “Add features 1, 2” 都不會算在這個Pull Request 裡面。

要修正這個問題,我們必須讓 upstream 跟現在這個 develop branch完全沒有共同祖先,我所知的有兩個解法:
一個是從開始的master branch開一個新的branch newDev,然後用cherry-pick 把develop 上面的 commit 都搬到這個branch 上,再重送 Pull Request;就如這篇文中所示:
http://stackoverflow.com/questions/25484945/pull-request-merged-closed-then-reverted-now-cant-pull-the-branch-again

$ git checkout master
$ git checkout -b newDev
For every commit in develop branch do
$ git cherry-pick commit-hash

另外一個方法比較方便,直接在 develop branch 上面切換到 newDev branch,然後使用 interactive rebase ,然後把所有的commit 都重新commit 一篇,newDev 就會變成一個全新的分枝了:
$ git checkout develop
$ git checkout -b newDev
$ git rebase -i master
Mark every commits as reword (r) in interactive setting

修正完之後的 repository 大概會長這樣


這時候就能由 newDev branch 對上游的 master 送出Pull Request,把這次的修改都收進去了。

題外話:
寫這篇時發現了這個工具,滿好用的,可以輕鬆畫各種git 的圖,雖然說試用一下也發現不少bugs XD,不過這篇文中的圖都是用這個工具畫的:
https://github.com/nicoespeon/gitgraph.js

2016年12月30日 星期五

使用git hook在commit 前進行unittest

使用 git 做為版本控制系統已經有一段時間了,最近在寫Facebook message viewer 的時候,就想到能不能在本地端建一個 CI,每次 git commit 的時候都會時自動執行寫好的測試?
查了一下就查到這一個網頁,裡面有相關的教學:
https://www.atlassian.com/continuous-delivery/git-hooks-continuous-integration
在這裡記錄一下相關的設定:

Git hook 可以想成git 的plugin system,在某些特定的指令像是commit, merge的時候觸發一個script。
Hook 可分為 client side 和server side,又有 pre-hook 跟 post-hook 的分別,pre-hook 在指令執行前觸發,並可取消行為;post-hook 得是在指令結束後執行,它無法取消行為只能做其他自動化的設定。

網頁中有給一些使用範例
Client-side + Pre-hook: 檢查coding style
Client-side + Post-hook: 檢查repository 的狀態
Server-side + Pre-hook: 保護master
Server-side + Post-hook: broadcast 訊息

所有的hook 會放在.git/hook 資料夾裡面,在開啟專案時就有預設的一些範例,只要拿掉檔名的 .sample就能執行,它們是從 /usr/share/git-core/templates/hooks/ 複製而來。
望檔名生義,這些檔名大多直接了當的指名它們的功用;有些script 如 pre-commit 在回傳非零值時,能阻止git 接受這次commit;詳細的使用方法跟觸發時機請見參考資料。

這裡我想做的是用 Client-side + Pre-hook ,在每次commit 前都先跑過一次測試,如果測試不過就拒絕此次commit,所以我們用到的是 pre-commit。
首先先在project 裡面加上一個test.py,用來統整所有的test script,這樣就可以用 python test.py 跑過所有測試,可以先在command line 測一下:
python test.py || echo $?

pre-commit script 就很簡單:
#!/bin/sh

python test.py || exit 1
這個|| exit 1 是要確保pre-commit script 一定以錯誤結束,如果這個測試之後就沒有其它測試就不需要這段,因為script 會回傳最後一行command 的回傳值。
另外,如果不想在打下commit 的時候出現 unittest 的輸出,可以改寫成:
#!/bin/sh

python test.py 2>/dev/null
if [[ $? -ne 0 ]]; then
  echo "> Unit tests DID NOT pass !"
  exit 1
fi
注意後面的判斷跟echo 是必要的,否則python 輸出導向null之後,使用者打git commit 會變成完全沒有反應,這顯然不是個好狀況;完成之後,試著下git commit,就會發現test 不過,而不像正常流程一樣跳出commit message編輯器:
Garbage@GarbageLaptop $ git commit
.F
======================================================================
FAIL: test_zh_tw (__main__.REdictParseTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 17, in test
assert(ans == parse)
AssertionError

----------------------------------------------------------------------
Ran 2 tests in 0.007s

FAILED (failures=1)

我的建議是在 test case 有一定規摸的時候,足以進行TDD 的時候才引入此流程,或者要先把test 裡都加上expected failure ,否則有test 不過就會讓git 根本commit 進不去,如果又因為過不了就每次都下git commit --no-verify,就失去TDD 的意義了
另外,請記得所有要跑的 script 一定要有執行權限,之前試了一段時間,每次commit 還是都commit 進去,後來發現是 pre-commit 沒有執行權限lol;hook 內的東西clone 的時候也不會複製到 client side,要把它們包到repository 裡面,再由使用者自己把script 加到.git/hook 裡。

這裡大致介紹 git hook 的簡單用法,網路上能輕易找到許多亂七八糟的script 來做各種事,例如用autopep8, cppcheck 檢查語法是否合標準,自動跑npm test 等等。

參考資料:
git hook in gitbook
https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
stackoverflow about pre-commit and unittest
http://stackoverflow.com/questions/2087216/commit-in-git-only-if-tests-pass

2016年12月13日 星期二

那些在工作上看到的各種東西

十二月小弟剛結束手邊的一個從七月延續到現在的案子,在這為期5個月的工作過程中,個人也有不少收穫,在這裡寫篇文章記錄一下。

溝通:

這次的案子是在不同的平台間移植,工作上沒遇到太多技術問題,在對方公司原有架構上,把平台相關的程式碼換成目標平台的程式即可,撰寫的程式碼不超過300行,只花大約一週半的時間,相關的程式碼就已經寫完了。
後來之所以會拖這麼久,一個原因是對方公司的程式碼還在不斷的更新,而我們必須將這些更新都整進我們的程式中,並且還要在機器上測試過。
這次真的是見證軟體工程,最重要的是團隊間的溝通,少了溝通再怎麼會寫也沒有用,整合上很多問題,諸如架構、測試、環境設定、測試硬體,都是由我的頭頂上司出面,多多溝通就解掉了,溝通的能力有時比技術能力還要重要。


架構:

整個過程中不斷撞牆的部分,在於整個Project 的開發過程中幾乎看不到架構設計的概念,感覺上就是東改一些西改一些,沒有一個系統性的規劃;甚至到現場準備產品發佈的前兩週,突然出現架構的大幅修正,我們整合的部分當然也要收進去,也因此在週末加了一天班(yay。
後來在言談中得知,對方公司的確沒有架構工程師這個職位,那個<架構的大幅修正>只是某個資深工程師這麼寫,其他人看看覺得不錯,其他專案也就跟著這麼做。
對方公司也算台灣有點大的硬體廠,軟體部門的狀況卻是如此,幾乎接近各程式設計師各自為政,知道了還是覺得有些心寒,不知道更上面的廠商狀況會不會好一點?

開發過程中,我自己也犯了類似的錯,應對方要求,我們這邊要整合兩個不同平台的程式碼,我竟然把所有的程式碼放在同一個檔案再用編譯macro 隔開,這當然不符合軟體工程的概念,只要加上更多平台,程式碼必定會增長到難以維護。
後來反省,的確是我的經驗不足才會這樣寫,真是幾年程式寫下來,軟體架構的概念還是沒練成,也難怪要成為架構工程師,都要先經過好幾年的軟體工程師的訓練。

測試:

這是專案中移植的目標平台,使用了一家中國廠商提供的SDK,無奈這家廠商有點兩光,通常會送有問題的程式碼過來,文件也寫得不清不……啊不對是通常沒什麼文件。
一開始在做整合的時候,因為sdk有問題的關係,我們的程式碼執行不定時間就會造成其他的process crash,從log 上來說就是系統上某process (而且每次的process 還不一定一樣)segmentation fault 了,跟這篇文章描述的有 87 % 像
當下無論是我們亦或對方公司覺得是自己的錯,但程式碼怎麼看都沒有問題,無奈它又是個不穩定的錯誤,在實際環境上測試非常花時間,在這個問題上面就花了近一個月的時間。
後來是利用一支之前用過的測試程式,可以用迴圈不斷執行核心功能,透過執行這條測試時也會crash,確認問題是出在核心功能而不是其他地方。

實際測試需要手動碰觸控螢幕,測試程式只需要用終端機執行,透過測試程式才能快速的註解掉可能有問題的部分,再測試會不會crash 來判定問題在哪,最後才定位出問題出在對方的sdk中,回報上去才拿到修正這個問題的新版sdk 。
覺得就算沒有要用到TDD,在專案中維持一些簡單的測試,光是簡單的回歸測試都能預防開發中可能的問題,就算真的出問題找不到在哪時,也能利用測試來定位錯誤。

能力:

這次發覺,自己的能力和對方公司的工程師比起來並沒有差太多,一些方面可能還超前對方。

很多工具例如git 的使用,如何透過Format patch同步兩個git repository 的狀態(當然對方公司內部是用svn ,所以這部分比較勝之不武),shell script 一些工具,我也比對方公司的工程師知道多一些些,工作的效率自然更高
我個人是認為,對方工程師雖然有較長的工作經驗,卻沒有-亦或沒有辦法-持續的和外圈技術圈交流,學習和知道一些最新的小工具:例如我在Code & Beer 聊天中知道的 autojump
一些無名的小工具能大幅提高生產力,卻未必能大紅大紫,是在小眾的技術圈中流傳,多多交流還是能有效擴展自己的見聞。

工具:

這次也學到,平時多多熟悉一些工具的使用,用到的時候可以發揮很大的助益
案子的結尾,有機會跟對方公司的工程師一起到客戶現場去準備產品上線,到現場是個很新奇的經驗,但工作環境實在不太好,程式碼不讓我們用版本控制就算了,連網路也不讓我們使用,遇到問題都只能問男人。
沒辦法查網路的時候真的是在比底力,平常有沒有熟悉shell 工具和程式語言這時立分高下。
我們又被現場公司雷:他們的環境裡有他們自己的設定,造成我們的code 放上去,clean 重build 的時候,把他們的設定移除,造成機器當掉,又是感覺起來是我們的程式有問題
其實只要把他們的設定再裝一次就解決掉了-但從沒人告訴我們,為了抓這個問題花了兩個工作天,大部分時間都在比較build log,在不同版本間進退,也因此用到不少 shell 工具。

我整理記憶上用得上的工具:
一套版本控制系統,至少要能做到在兩台機器上同步,我是用 git
至少一款diff 工具,我是用 vimdiff
檔案關鍵字的搜尋工具:shell grep
找檔案工具:shell find
簡單的 shell script,一旦發現到有重複性的工作,可以的話就盡快改寫成shell script,可以加速工作的效率。
上面這些除掉網路還要能熟練地使用。

總結上面五個,這五個月來接這案子的小小心得,記錄一下。


註:照片有不少篇幅都是用Google Doc, Voice Typing 的功能打的,覺得直接用講的雖然可以省下打字的時間,還有簡省手力,不過事後校稿也滿耗時的。
感覺用說的句子跟用手打出來的風格不太一樣,句子拉得比較長,贅字跟語助詞較多,如果是先寫在紙上再打到電腦又會是另一種風格,挺有趣的。

2016年11月26日 星期六

minecraft 創作:莒光樓

小弟是2198T 的退伍金門兵,當兵時放島休,自然會到金門島上四處走走,覺得莒光樓是個不錯的建築題材,退伍後花了一點時間在minecraft 裡面蓋了一座,斷斷續續的蓋,工期大概6個月。
在陰間沒相機,只能畫畫草圖,幸好google 找圖片就有很多人拍的相片可以看,庭園的部分用google 地圖也有實景可以「鍵盤旅遊」,很多細節都是這樣補齊的。

外觀好像沒什麼好說的,照著照片依比例選用磚塊、雕紋磚塊、玻璃等打造,室內擺設除了關鍵位子的盆景可以放之外,其他大部分都只有掛畫,都快變畫廊了。


二樓外部裝飾,同樣用Banner 掛在spruce fence來實作,缺點是超級浪費染料,染料也不像方塊挖一挖就有了,用掉一堆骨粉跟打一堆草,紅染料需求大產量又小,打草打到OOXX
後來才想到伺服器上其實有人蓋了鐵人農場,紅花都用不完_(:з」∠)_
其他部分:
藍旗 + (白染料 + 磚塊)+ (紅染料456)
藍旗 + (白染料 + 磚塊)+ (紅染料456)+ (白染料123)+(黃染料789)+(白染料468)

莒光樓的匾的部分,寫字真的就無法了:
白旗 + (藍染料741/963)+(藍染料123)+ (紅染料456)
藍旗 + (紅染料456)+(白染料258369)+(藍染料123)

屋頂的藍色後來發現是海中prismarine 的顏色最像,其他部分就用spruce 跟dark oak 來打造。

遠眺福建省……嗯……算了當我沒說,只會看到巨大農場……

天花板大概是最麻煩的東西了,原本的天花板是雕梁畫棟五彩繽紛,如照片所示,這在Minecraft 裡面根本做不到,只能用各種色彩的羊毛意思意思一下;只能說這中華民國美學你敢嘴?像素太低了沒辦法啦(yay

庭園的部分,沒什麼就是一直種樹,超級花時間;在實際莒光樓,沒人會去坐,不知道幹什麼的樓梯,都靠這個幫我算圓形怎麼蓋
http://donatstudios.com/PixelCircleGenerator
圍牆的部分就選用End Stone Brick ,顏色跟質料都很像:


花園蓋得比需期得還要大了一點,原本估計的正方形 72x72,後來蓋到快110x110,大超多owo,幸好server 上有安裝自己寫的fastbuild (自肥一下),在整地、填土跟挖建材上省了很多時間。
https://github.com/yodalee/FastBuild

來張全景:


建築物蓋完用shader 來一張是一定要的:
正面:
後面:
 

這個建案大概就到這邊了,還有很多細節部分無法盡善盡美Orz
金門其實是個漂亮的小島(只要不是當兵的話),歡迎大家去金門旅遊,小小建案希望大家喜歡owo

2016年11月21日 星期一

利用lxml 實作高效率的parser

最近實作facebook message viewer 的時候,需要去處理相當大的html 檔案,原始檔案大小約50 MB,beautify 之後會加到近80 MB。
實作上我用lxml 來實作,用了最基本的寫法,最直覺而簡單的寫法:
from lxml import etree
parser = etree.HTMLParser(encoding='UTF-8')
root = etree.parse("largefile.htm", parser)
問題是什麼呢,這樣lxml 一開場就把整個htm 給載到記憶體裡面,消耗一堆記憶體。
我們執行一下,看到的結果是這樣:
group count 525
parse 280895 entries
./parser.py   25.80s  user 0.21s system 99% cpu 26.017 total
  avg shared (code):         0 KB
  avg unshared (data/stack): 0 KB
  total (sum):               0 KB
  max memory:                710 KB
  page faults from disk:     0
  other page faults:         181328
710KB!根本記憶體不用錢一樣在浪費…,我把這個丟到網路服務上,光記憶體就害自己被砍了。

要打造節省記憶體的 xml parser,相關的資訊都來自這篇:
http://www.ibm.com/developerworks/xml/library/x-hiperfparse/

文中提供了兩種方式,一種是在parse 中指定我們的target class,我們要實作一個class 包含下列四個method:
1. start(self, tag, attrib): 有tag open的時候會呼叫這個method,這時只有tag name跟attribute 是可用的
2. end(self, tag): tag close 時呼叫,此時這個elem 的child, text都可以取用了
3. data(self, data): 在parser 收到這個tag 中的text child的時候,會將text 變為作為參數呼叫這個method
4. close: 當parse 結束時呼叫,這個method 應該回傳parse 的結果:
之後取用
results = etree.parse(target=TargetClass)
results 就會是close method return 的結果,這個方法不會消耗大量記憶體,不過它的問題是每一個遇到的tag 都會觸發一次事件,如果大檔中之有少量elements 是想剖析的,這個方法就會比較花時間;我的案例比較沒這個問題,因為我要剖析的內容幾乎橫跨整份文件。

另一個方法是用lxml 本身提供的 iterparse,可是傳入指定events的tuple跟一個有興趣的tag name的list,它就只會關注這些tags 的events:
context = etree.iterparse(infile, events=('end',), tag='div')
for events, element in context:
  print(elem.text)
文中並有說,iterparse 雖然不會把整個檔案載入,為了加速整份檔案可能被多次讀取的狀況,它會保留過去element 的reference,因此剖析會愈跑愈慢;解決方法是在iterparse 中,取完element 的資料就把它給刪除,同時還有element已經剖析過的sibiling,在iteration 最後加上這段:
element.clear()
while element.getprevious() is not None:
  del element.getparent()[0]
要注意的一個是,使用iterparse 的時候,要避免使用如getnext() 方法,我用這個方法,有機會在剖析的時候遇到它回傳None,應該是iterparse 在處理的時候,next element 還沒有被載入,因此getnext 還拿不到結果;從上面的end 事件來說,每個iteration,就只能取用element 裡面的元素。

改寫過之後的parse 的執行結果:
group count 525
parse 280895 entries
./parser.py   22.59s  user 0.07s system 99% cpu 22.669 total 
  avg shared (code):         0 KB
  avg unshared (data/stack): 0 KB
  total (sum):               0 KB
  max memory:                68 KB
  page faults from disk:     0
  other page faults:         21457
跟文中不同,因為我有興趣的tag 比例較高,執行時間沒有下降太多,但記憶體用量大幅下降。

另外文中也有提到其他技巧,像是不要用find, findall,它們會用XPath-like expression language called ElementPath。
lxml 提供iterchildren/iterdescendents 跟XPath,前兩者相較 ElementPath速度會更快一些;對於複雜的模式,XPath可以經過事先compile,比起每次呼叫元素的xpath() method,經過precompile 的XPath在頻繁取用下會快很多,兩種寫法分別是:
# non-precompile:
xpathContent = "//div[@class='contents']"
content = root.xpath(xpathContent)
# precompile:
xpathContent = etree.XPath("//div[@class='contents']")
content = xpathContent(root)
下面是我實測的結果,在都是完全載入記憶體下,有無precompile 的速度差接近兩倍,不過這也表示我用了iterparse 其實速度是下降的,因為我iterparse 其實是有使用precompile 的:
./parser.py 14.99s user 0.35s system 99% cpu 15.344 total
./parser.py 25.82s user 0.40s system 99% cpu 26.232 total

以上大概是使用lxml 時,一些減少記憶體用量的技巧,記錄一下希望對大家有幫助。

題外話:
在寫這篇的過程中,我發現Shell script 的time 其實是一個相當強大的指令,除了時間之外它還能記錄其他有用的數值,像是 memory usage, I/O, IPC call 等等的資訊。
我的time 是用zsh 下的版本,我也不確定它是哪裡來的,用whereis 找不到,用time --version 也看不到相關資訊,但man time 有相關的說明,總之copy and paste from stack overflow,把我的TIMEFMT 變數設定為:
export TIMEFMT='%J   %U  user %S system %P cpu %*E total'$' \
  avg shared (code):         %X KB'$'\
  avg unshared (data/stack): %D KB'$'\
  total (sum):               %K KB'$'\
  max memory:                %M KB'$'\
  page faults from disk:     %F'$'\
  other page faults:         %R'
其他的變數選項可以在man time 的format string 一節找到,這裡就不細講了,算是寫code 的一個小小收穫吧。