ThreadPoolExecutor 就是個坑

主要問題可參考這個 issue,裡面有以下片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import concurrent.futures as cf

bucket = range(30_000_000)

def _dns_query(target):
from time import sleep
sleep(0.1)

def run():
with cf.ThreadPoolExecutor(3) as executor:
future_to_element = dict()

for element in bucket:
future = executor.submit(_dns_query, element)
future_to_element[future] = element

for future in cf.as_completed(future_to_element):
elt = future_to_element[future]
print(elt)

run()

可發現這是照著文件例子寫的,只是數量增加一下,記憶體就爆了。問題乍看出在 future_to_element 長太大,但就算不把 submit() 的結果放在容器中,只要在 with clause 中就一樣會佔用記憶體。也就是說在工作數量大的情況下,ThreadPoolExecutor 並不實用。Issue 中建議是另外加個 loop 把總工作切成一批批的處理,每次處理一批就要等到全部跑完,然後換下一批。若為了節省時間把批次處理量加大,記憶體又消耗很多,不是很理想。

這其實是不必要的:future 可用以得知是否出錯、以及工作完成的結果,但要知道結果有其他機制可以利用,最典型的例如送到 queue,問題就只剩錯誤處理。先前在「出錯就盡快停止」的條件下,寫過類似下面的程式片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import threading
import typing


class ThreadExecutor:

def __init__(self, max_workers: int):
self.max_workers = max_workers
self.semaphore = threading.Semaphore(max_workers)
self.last_error = None

def _wrapper(self, fn: typing.Callable, args, kwargs):
try:
fn(*args, **kwargs)
except Exception as e:
self.last_error = e
raise e
finally:
self.semaphore.release()

def submit(self, fn: typing.Callable, *args, **kwargs) -> bool:
if self.last_error:
return False
self.semaphore.acquire()
threading.Thread(
target=self._wrapper, args=(fn, args, kwargs)
).start()
return True

def join(self):
for _ in range(self.max_workers):
self.semaphore.acquire()
if self.last_error:
raise self.last_error


def task(x):
import random
import time
if random.random() < 0.001:
raise RuntimeError(x)
print(x)
time.sleep(1)
return


def main():
excr = ThreadExecutor(100)
for x in range(500):
if not excr.submit(task, x):
break
excr.join()


if __name__ == '__main__':
main()

如此不用批次也沒有消耗記憶體問題。

但以上提的還算是表面。最終是會需要 Scala concurrent.Map 那樣的機制,而且必須是 lazy,或是 Go 用 channel 也能處理得很漂亮。只能說 Python 在 concurrency 領域還差得很遠,這也限制了它作為 backend, infrastructure 等等應用的普及度。

用虛擬 webcam 作為線上開會視訊 aka v4l2loopback on Ubuntu 18.04

會找這個來用是因為想把線上開會時的視訊整個替換掉。v4l2loopback 會在系統上做出 一個假的 webcam device,可以當成真的 webcam 來用,播放餵給它的東西。套件名稱是 v4l2loopback-utils 會依賴 v4l2loopback-dkms 一起裝進來,但版本太舊,跑起來問 題一堆,所以直接去 github 那邊抓了最 新 release v0.12.4 來用。

安裝就直接照 README.mdmake 然後 insmod v4l2loopback.ko 下去就行了。講 究一點可以照裡面說的跑 depmod -a 之類的讓它自動帶 dependencies. 先設定:

1
2
v4l2loopback-ctl set-caps 'video/x-raw,format=UYVY,width=640,height=640' \
/dev/video2

/dev/video2 是系統上 insmod 以後產生出的 device name, 然後 640x640 可以改成你 想要的解析度。

1
2
3
gst-launch-1.0 -v filesrc location="fakeportrait.jpg" \
! decodebin ! imagefreeze ! videoconvert ! videoscale \
! identity drop-allocation=1 ! v4l2sink device=/dev/video2

這例子是把一張靜態圖片做背景。用 zoom 可以看到沒問題,用 cheese 碰到問題就是不讓 我換預設 device,已經找過不用 cheese -d /dev/video2 改用 cheese --device="Dummy video device (0x0000)" 還是有問題,乾脆砍了。

awesome window manager: choose default layout

awesome window manager 也許多年了,一向都蠻順手,這 陣子單獨用筆電作業的時間比較多,也就是沒外接螢幕跟鍵盤,總覺得卡卡的。原因是筆電 螢幕解析度高,但相對應的 DPI 也調到正確值 210,一行大約 140 個字母。由於習慣一行 最多 80 個字母,所以如果螢幕寬度無法容納 160 個字,視窗就不適合用左右並列的方式。 在 awesome 的 rc.lua 裡原本是這樣寫:

1
2
3
4
5
if s.geometry.width > 1600 then
layout = awful.layout.layouts[1] -- 左右並列
else
layout = awful.layout.layouts[2] -- 上下並列
end

這樣會按照螢幕橫向解析度決定要不要並列,但現在不適用了,因為雖然筆電螢幕寬度是 2560 點,但並不希望是左右並列。解決方法是實際去計算能放幾個字。慣用的 adobe source code pro 10pt, 72pt 是一英吋,然後這字型是設計成寬度大約是字體大小的 60%, 最後加入 0.25pt 當成字距用以微調。

1
2
3
4
5
if (s.geometry.width / s.dpi) / ((10 * 0.6 + 0.25) / 72 )) >= 160 then
layout = awful.layout.layouts[1]
else
layout = awful.layout.layouts[2]
end

實際用幾組不同螢幕規格試算,結果都還符合預期。

讓 pmount 支援 exfat

一直覺得 pmount 沒支援 exfat 很麻煩,剛弄了個一行的 patch.

抓下來以後可以這樣做一個自己的 package 裝起來。

1
2
3
4
5
6
apt-get source pmount
cd pmount-0.9.23
quilt import ~/Downloads/exfat.patch
debuild -i -us -uc -b
cd ..
sudo dpkg -i pmount_0.9.23-3build1_amd64.deb

2020/09/23 更新: 不知有什麼改變了,總之現在要多個 nonempty 才會動。patch 已更新。

12 pt 的字,到底是多大

(重刊 2017/12/18 在 MMDays 的文章)

這幾天筆者加購了一台配備高解析螢幕的筆電,由於平時使用的並不是 Apple 系統,且筆電算算也有六年沒換,雖說蹭別人的 Mac Book Pro 玩過幾次,以高解析螢幕為主的使用經驗,在此之前還真沒有過。筆電來了,自然要把附帶的 Windows 10 縮一縮,裝上慣用的 Ubuntu 才是正辦。過程中,又碰上老問題:字太小。

這問題快十年前就碰過,大約知道是 DPI 設定不正確。雖說本行不是幹這個,但為了顧眼睛還是得處理一下,還可寫篇文章談談不專業的豆知識交差。先從字型大小說起。

字型尺寸當然是從電腦出現之前就存在,pt 是 point 縮寫,雖在歷史上幾經變遷,最終在數位出版時代訂立 desktop publishing point,長度為 1/72 英寸,大約 0.353 公釐。說到這似乎已說完了:那麼,12 pt 字,當然就是 12/72,也就是 1/6 英寸(又稱 1 pica),會有什麼問題呢?

然而對電腦使用者而言,卻往往不是如此。我們知道 12 pt 的字,用印表機印出時,大小是固定的。但在一般解析度螢幕上,字比較大,而在高解析度螢幕,字就變小了。這似乎可以自圓其說:高解析度下,一個螢幕上的「點」比較小,所以如果螢幕上的字是由固定點數所組成的,那麼每一點越小,字當然就比較小了。早期的字型的確是這樣的,叫做點陣字,也的確會有在不同解析度大小不一致的問題。但後來向量字型出現,可以平順的縮小放大,也開始用 pt 為標示單位,為何還是有大小不一致的情況呢?

要說明這個,就必須先說明 DPI 是什麼1。DPI 是 dots per inch 的縮寫,表示每英寸點數。數字越大,解析度越高。例如印刷常用的 300 dpi,看起來已經非常清晰,一般 24" 的 1080p 螢幕是 92 dpi,Apple 的 Retina 筆電多半落在 200 多,而 iPhone 由於觀看距離更近,可高達 300 多,iPhone X 更高達 458 dpi (註 1)。有了 DPI 概念之後,再回去想想 pt 的問題,就比較清楚了:假定某印表機以 300 dpi 列印,那麼 12 pt,也就是 1/6 寸的字型,其大小就是 50 點 (dots),而若是在解析度比較低的螢幕,例如 90 dpi,就是 15 點。如此,同樣的字型尺寸,顯示在不同解析度的螢幕上,就是一樣大,只是精細程度不同。

這麼說來,只要知道螢幕 DPI,字型大小的問題照理說就可以得到解決,但為何沒有呢?這又是另外一個故事。1980 年代,Apple Macintosh(麥金塔)電腦設定的解析度是 72 dpi,讀者可發現這跟 point 單位一致,也就是 1 point 剛好對應到 1 dot,12 pt 在螢幕上就用 12 dots 表示。當時搭配的螢幕是 512 x 342,換算下來寬度大約是 7.1 in,而北美慣用的 letter 大小紙張寬度 8.5 in,兩邊扣掉 0.7 in 的留白,恰好是 7.1 in,所以若螢幕不設定兩邊留白,字型大小就會完全一致。這乍看是非常合理的設定,但也產生一些問題。例如 12 pt 的字,在 300 dpi 下,每個字可以用  50 點表示,相當足夠,但在螢幕上,就只剩 12 點,加上英文字母上下有突出,主要的部份例如字母 x,只剩下 6 點可用,看起來就比較粗糙。再加上螢幕跟眼睛之間還有鍵盤,一般觀看距離比紙張來得遠,字看起來就更小了。

這時,微軟想出了一個聰明 (?) 的方法來改善這問題:相對於 Apple 的 72 dpi,他們增加 1/3,在所有軟體中將解析度設定為 96 dpi. 短期內這有幾個效果:若需要 10 pt 的字,解析度是 72 dpi,就只能用 10 點來設計字型,看起來比較粗糙;而若解析度是 96 dpi,就有 13 點,設計上會更容易些。而在顯示時,由於當時螢幕一般都還在 72 dpi 的水準,96 dpi 會導致螢幕使用比所需更多的點數來顯示,讓字母高度變成原先的 4/3 倍,看起來比較清楚。顯而易見的缺點是,這背離了原本 DPI 與 pt 的關係,而隨著 Windows 3.1 大紅,到 Windows XP 長達十多年的統治,這也造成相當深遠的影響。在 Windows 7 之前,預設的 96 dpi 並不會隨著不同螢幕而改變,這也造成前面所提到的,高解析螢幕字比較小的印象。

為了解決這問題,後來微軟發展出 device independent pixel (DIP) 這單位,其定義就是每 DIP 為一寸的 1/96。沒錯,就是 96 dpi 這個魔術數字。舊的 Windows 應用程式,其基本假設就是畫 96 個點是一寸,那麼在高解析螢幕上,只要將其使用的「點」定義為 DIP,就可以保持這程式的界面仍然可用。這也是為何一些比較古早的應用程式,看起來有點糊糊的,因為 Windows 自動以 DIP 放大了原先界面的緣故。

讓這筆糊塗帳更加糊塗的,還有另一個當初看起來很有道理的設計:網頁的 CSS 規範。CSS 全名為 Cascade Style Sheet,就是一般網頁使用的排版格式,其內有個常見的長度單位,px, pixel unit. 以 1998 年公佈的 CSS level 2 來說,CSS 的長度單位分成兩種,絕對與相對。絕對單位包括了 in, cm, mm, pt, pc (pica) 等等,可注意到也有熟悉的 pt,而相對單位則有 em, ex, px,其中 em, ex 是相對於設定的字型大小,例如 1 em 就等於設定的字型大小,所以若設定字型大小為 12 px,那 1 em 就是 12 px. 而 px 則是相對於「顯示裝置的解析度」,也就是螢幕上的一點。由於各個螢幕一點的大小不見得一致,px 就成了相對單位。在 CSS 2 裡面,針對解析度不同以及觀看距離不同的情況,還建議了參考的改變方式,這容後再論。

在 199x 的年代,一般螢幕的解析度不是那麼高,設計上差了幾點,看起來就差很多。例如一條線寬度是一點或兩點,看起來會有相當明顯的差異。所以,設計上會講究 pixel perfect,也就是仔細控制網頁上各個元件長度是幾「點」,而不使用絕對單位。在指定字型時,往往是類似 font-size: 12px 這樣的形式,而其他諸如欄位寬度等等,也一律使用 px 來設計,以確保顯示出的結果符合預期。但這造成一個問題:網頁上的長度單位,包括字型,又再度隨著螢幕解析度而改變了。在 72 dpi 的螢幕上, 12 px 就是 12 pt,沒什麼問題,但螢幕的 dpi 一直在提昇,12 px 的字也就越變越小,只能創造出各式各樣觀念去修正。

在這裡就要提到,前面 CSS 2 針對不同解析度及不同觀看距離,所做的建議。其內容是,1 px 應等於在一般觀看距離,即平均手臂長度,定義為 28 寸,觀看 90 dpi 的螢幕,螢幕上 1 點的大小。也就是說,1 px 從「長度」變成了「角度」,約略等於 0.0227 度,投射出的距離,是會隨著觀看距離而改變的。底下這張圖,直接來自 CSS2:

這裡顯示出,在 28 寸的距離,1 px 約等於 0.28 mm,而在 140 寸(例如投影情況),1 px 約等於 1.4 mm,是原先的 5 倍。複雜歸複雜,至少這裡對於 1 px 提出一個還不錯的標準,希望在不同的觀看條件下,都能得到一致的經驗,只是事與願違,後來大部分實作都還是採用 1 px 對應螢幕上一點的設計,且 96 dpi 隨著 Windows 四處普及,大家都把 96 px 當成實作上需要 1 寸的約略單位,不但應用程式與網頁如此,連螢幕也是,以避免使用者由於 dpi 差距過大而難以使用。在實作日漸偏離標準的情況下,CSS 2.1 這修正版裡,針對這部份有幾個修改,都相當有趣:

  1. 參考用的 px,從原先定義的 28 寸,90 dpi,改為 96 dpi。也就是說,W3C 肯認大部分螢幕都設計成接近 96 dpi,所以在預設的閱讀距離,也就是 28 寸時,參考用的 px 約略就等於螢幕上的一點。

  2. 將 px 從相對單位,改為絕對單位,長度是 0.75 pt. 如此一來,在 96 dpi 的裝置上,96 px = 72 pt = 1 in,就一致了。

  3. 讓 px 有雙重定義:在接近 96 dpi 的螢幕上,px 保持在 0.75 pt,也就是約略仍等於螢幕上的一點。而在其他情況,則採用角度的定義,而讓其他單位如 pt 等「相對於 px」定義出來。亦即,假設現有一 200 dpi 螢幕,在 28 寸位置,若採用角度定義,px 就不再表示螢幕上的一點,而是約略等於兩點,而 pt 由於相對於 px,就仍然可以約略等於 1/72 寸。而由於是角度,所以只有在 28 寸的觀看距離,pt 才會約略等於物理上的 pt 單位。若在不同距離,例如預設的觀看距離比較近的 iPad mini 與 iPhone 等,網頁上的 pt 與物理上的,就不會相等了。

繞了這麼一大圈,回到一開始的題目:新筆電的 DPI 設定。以下是設定前與設定後比較,這也可以看出,Apple 當時提倡 Retina 螢幕,打破 96 dpi 魔咒,還要維持各類程式、網站等仍然可用,實在是相當了不起的突破,也推動其他廠商跟進,最終造福使用者。

govendor vs dep

我的 ~/.gitconfig 裡面有這樣的設定

1
2
3
4
[url "git@github.com:"]
insteadOf = https://github.com/
[url "git@bitbucket.com:"]
insteadOf = https://bitbucket.org/

作用是存取 private repository 的時候,會自動換成 git+ssh. 本來不需要設定這個, 因為 git clone 的時候都會特別注意網址,只是為了方便使用 go get,因為它對這兩 個站預設只會用 https://,碰到 private repo 的時候就沒法自動了。

但剛才抓別人的 project 下來以後跑 govendor sync 卻發現它針對 bitbucket.org/private_team/private_repo 這樣的情況沒法處理:

1
2
3
$ govendor sync
Error: Remotes failed for:
Failed for "bitbucket.org/private_team/private_repo" (failed to ping remote repo): https://api.bitbucket.org/1.0/repositories/pilot_seals/quividisdk: 403 Forbidden

改為試著執行 golang 官方做的 dep,竟然會自己讀 govendor 用的 vendor/vendor.json 檔案來轉換,而且順利的下載了 private repo 執行完畢。

找了一下原因,發現 govendor 裡面用了官方建議的 golang.org/x/tools/go/vcs 來 處理 bitbucket 的 repo 類型判斷,然後 vcs 裡面會呼叫上面的 API 網址;但如果是 private 的,就連那網址都會 403,導致噴錯。奇怪的是,dep 就沒有這個問題。看了看 程式碼,發現它用的不是官方那份 vcs,而是來自 github.com/Masterminds/vcs 的另 外一份,說明寫說是基於 vcs 擴充而成的。看一下 bitbucket 相關程式碼,發現多了這 段:

1
2
3
4
5
6
7
8
9
10
// Fast path for ssh urls where we may not even be able to
// anonymously get details from the API.
if ul.User != nil {
un := ul.User.Username()
if un == "git" {
return Git, nil
} else if un == "hg" {
return Hg, nil
}
}

這也太黑心了,dep 自己沒用官方那份,這樣 govendor 豈不是死得不明不白…

2018/07/24 補充

golang.org/x/tools/go/vcs 是從 cmd/go/internal/get copy 出來的,而 upstream 已解掉此問題:#5375,程式碼在 vcs.gogovendor 本身的 issue 在 #151.

Intel SSD 535 480 GB Shows as "sandforce 200026BB"

症狀為使用中突然硬碟消失,重開機硬碟有一定機率無法開機,且被辨識為 sandforce 200026BB. 若能正常開機,使用一段時間後又發生相同情形。一開始不能確定是主版或 SSD問題,又是重要的工作用機器,就先訂了新的筆電。沒想到最後修好…

先照著 Intel support 上面的作法試著更新韌體,但發現原先版本已經是 RG20 無法 更新。接下來試著從首頁進去直接找最新韌體,用 dd if=issdfut_2.2.3.iso of=/dev/sdb bs=1M 做隨身碟之後開機,成功更新到 RG21,竟然就沒問題了。雖然好像應 該查一下 change log 看看究竟是什麼問題,不過懶病發作,算了。

皇帝的舊衣

Fermat’s Library 觸發讀了 C. A. R. Hoare 這篇論文[1],其中 最常被引用的一段話應該是:

I conclude that there are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies.

The first method is far more difficult.

「我斷定軟體設計有兩種方式:第一種是非常簡單,所以明顯的沒有缺點。另一種是非常複 雜,所以沒有明顯的缺點。前者困難多了。」

最後的小故事也很有意思,簡中有翻譯但實在不好,試譯如下。

皇帝的舊衣

很久以前,有位皇帝非常喜歡衣服,把所有錢都花在買衣服上。他沒去管士兵、赴宴、或審 判等麻煩事。若是提到別位皇帝或國王,人們可能會說「他正出席議會」,但提到這位皇帝, 人們總是說「他在更衣室裡」。而他真的都在裡面。在一次不幸的場合,他因被騙而裸體示 人,臣民覺得好笑,而皇帝非常懊悔。於是他決定再也不離開王座,且為了避免再次裸體, 他規定往後每件新衣服只能覆蓋在原有的衣服上。

在首都這大城中,時間快樂地過去了。部長與大臣、織工與裁縫、訪客與臣民、縫工與繡工, 為了各類事務在王座室進進出出,而他們全都大聲讚嘆:「皇帝的盛裝真是華麗!」

有一天,皇帝最老也最忠誠的部長聽說有位出類拔萃的裁縫,受教於一間古老的高等針線術 學府,他發展出一種新的抽象刺繡藝術,其針腳如此精細,以致沒人能分辨到底存不存在。 於是部長想:這繡法一定非常傑出,若能聯絡上這位裁縫擔任顧問,我們就能將皇帝裝飾的 炫耀程度提升到一個新高,世界都會認可他是自古以來最偉大的皇帝。

所以這位誠實的老部長花了一大筆錢,把裁縫請來。裁縫被帶到王座室,在那裡他對如今已 完全蓋住王座的華服致敬。大臣們渴望地等著他的建議。所以當他提出不要再增加更精細複 雜的刺繡潤飾,而是移除層層華麗衣飾,追求簡單優雅來代替複雜細節,你可以想像到大臣 們的震驚。他們交頭接耳:這裁縫並不是他自稱的專家,他的智慧已經被象牙塔中的長時間 研究搞混,無法了解現代皇帝的縫紉需求了。裁縫大聲爭辯了許久,說他的建議是合理的, 但大臣們聽不進去。最後,他接受了酬勞而回到象牙塔中。

直到此日之前,都無人揭露這故事的全部真相:在一個晴朗早晨,皇帝覺得很熱又無聊,於 是小心翼翼地從堆積如山的衣服下鑽出,現正在另一個故事中當個快樂的養豬人。而裁縫被 封為所有顧問的守護神,因為儘管他收了一大筆顧問費,卻從未成功說服客戶相信他的明悟: 他們的衣服下,沒有皇帝。

[1] Charles Antony Richard Hoare. 1981. The emperor’s old clothes. Commun. ACM 24, 2 (February 1981), 75-83. DOI=http://dx.doi.org/10.1145/358549.358561

Scroll Lock as Caps Lock

我的 Ubuntu /etc/default/keyboard 裡面有這設定:

1
XKBOPTIONS="terminate:ctrl_alt_bksp,ctrl:nocaps"

作用是「外加」一些選擇性的鍵盤設定。其中 terminate:ctrl_alt_bksp 這是 dpkg-reconfigure keyboard-configuration 就有選項可以設定,用途是這三個鍵一起按 就可以跳出 Xorg。而 ctrl:nocaps 則是手動加上,印象中圖形界面也可設定,但很久沒 用已不記得。這選項用途是把 Caps Lock 當成 Left Ctrl 來用。這樣設定的原因是,慣用 的編輯器 Emacs 使用時實在太常按 Ctrl,但筆電上用小指按久了會不舒服,所以用 Caps Lock 來代替。而原先的 Left Ctrl 功能仍然保留,因為外接實體鍵盤時用掌緣壓 Ctrl 很容易, 且筆電上有時仍然會用掌緣按而不用小指。

這樣設定是蠻順手,但仍有個缺點,就是沒有 Caps Lock 可用。比如要定義常數常常是 ALL_CAP_CONST 這種命名,一直按著 Shift 就有點不方便。

想到一個解決方案是拿真的沒在用的 Scroll Lock 當作 Caps Lock,偶爾要用時還有個鍵 可以按,於是想把這設定也做成一個 XKBOPTIONS,步驟如下。

首先新增 /usr/share/X11/xkb/symbols/local

1
2
3
xkb_symbols "sccaps" {
replace key <SCLK> { [ Caps_Lock ] };
};

接下來修改 /usr/share/X11/xkb/rules/evdev,在 ! option = symbols 這段最後面 補上

1
local:sccaps          =       +local(sccaps)

以及 /usr/share/X11/xkb/rules/evdev.lst, /usr/share/X11/xkb/rules/evdev.xml 都按照原先的格式加入 local:sccaps 這選項。最後把 /etc/default/keyboard 那行改為

1
XKBOPTIONS="terminate:ctrl_alt_bksp,ctrl:nocaps,local:sccaps"

重跑 dpkg-reconfigure keyboard-configuration 就生效。