跳到主要內容

火災

· 閱讀需 1 分鍾

6月6日晚上,我從廚房洗完碗到客廳,突然聞到一股難聞的燒焦味。沒有多想,我打開門查探情況,外面濃煙充斥,我趕緊關門,但還是滲了不少進來,客廳一股“硫”味。我才意識到真的發生火災了。我有些慌張,心裡開始想我會不會死掉,一邊找房東問物業電話打探情況。臥室的味道比較淡,窗外也看不到煙火,想來是另一側的火災。我縮在臥室飄窗,守著窄小的窗口呼吸外面的空氣。窗外有10釐米左右的地沿,我想要是燒進來了最後還可以站到那裡多堅持幾分鍾,又想起短視頻裡刷到的,站在那裡最後摔下去的畫面。

物業說火已經滅了。著火的是25樓另一側的臥室,我們住27樓。客廳仍有刺鼻的味道,驚魂未定的我們在臥室裡打遊戲刷劇:不然還能怎樣呢。

微波爐

· 閱讀需 1 分鍾

今天中午公司來了一個奇怪的人。沒有人認識他,他是來借用微波爐熱飯的。辦公室的人大部分出去吃飯了,我們剩下的幾個人都驚呆了,一時忘了回應,他又問了一遍。

雖然覺得很怪誕,但也沒人說得出拒絕的話。盤問之下了解到他是對面新公司的人,辦公室還沒來得及裝微波爐,他自己帶了飯沒地方熱。

竟然還可以這樣?如果是我大概會把飯留到回家吧!雖然我肯定不會在不明確公司環境的情況帶飯...

不過需要幫助的時候,為什麼不嘗試一下呢?通常陌生人都比想象的要更友好一些。雖然但是,下次如果有另一個人來借微波爐,我大概率會拒絕他吧👉👈

開遍英國每一條路

· 閱讀需 2 分鍾

screenshot.jpeg

《極限競速:地平線4》買了幾年,斷斷續續加起來70h+了,是我目前玩得最久的遊戲。我對遊戲的熱情並不高,很難想象遊戲社區那些一個遊戲動輒數百小時的玩家怎麼做到的...

大多數時候我都只是開著我最愛的保時捷四處轉悠,不參加比賽也不完成任務,今天突然發現只剩最後幾條公路沒有去過了,於是打開並不宏大的世界地圖,四處搜尋沒有走過的道路。地圖的大部分地方我都有印象,但閒逛的時候還是經常有新的體驗,打破一些個人記錄啦,發現新的車房寶物啦,碰到遺漏的廣告牌啦(我一般不會刻意去找)。大路熟悉,小路陌生,風景優美的地圖,在同一個地方用同樣的姿勢漂移翻車。在引擎的轟鳴聲中或衝刺或慢搖,開過高山,城市,和大海,這個遊戲總讓我感到一些生活的藝術性。

今天開遍了遊戲地圖的每一條公路,成就彈出來的時候有種恍如隔世的感覺,地平線5已經入庫了,地平線6也上架了,我還在地平線4流連忘返,還有多少地方我沒去過呢。

我是在Steam平臺遊玩的,地平線5多了手柄扳機震動,體驗更好,地圖也更大了,還有中文配音,只是最好的永遠是初見。

做了個圖片壓縮小工具

· 閱讀需 2 分鍾

寫博客時不可避免要上傳圖片,我現在的圖片都上傳到部署在家裡小主機的minio服務。自己當家才知道財米油鹽貴呀,每一MB空間都會增加備份負擔和流量壓力,動輒數MB的圖片也很考驗服務器帶寬。但其實我並不關注圖片的局部細節,只是想有個花花綠綠的圖而已。

由於我的圖大多都是手機拍的,我希望直接在手機上完成壓縮。找了現存的圖片壓縮app,要麼臃腫肥大參數繁雜,要麼有廣告、訂閱,我只是想把相冊的圖變小一點,僅此而已。

github找了一圈沒有特別對眼緣的,但我找到了Curzibn/Luban,一個模仿微信的智能壓縮庫,很符合我的要求,不用自己調參數,不過它是純工具庫,沒有app,於是我自己擼了一個魯班壓圖

luban_imager.webp

功能單一,打開圖片,壓縮,保存,僅此而已。

另外推薦一下Flutter,開發安卓app很絲滑,不像ReactNative那樣各種奇怪的小問題,而且包體積很膨脹。這個app也有十幾MB了,懷念以前幾百KB的小app〜

繡球

· 閱讀需 2 分鍾

某天晚上心情不好,去公園散步。回來的時候路邊看到一個巨大的“花球”,很圓,由一朵朵小花組成。我忍不住拍了拍,很緊湊,有彈性。

後來和朋友說起,她說可能是繡球。可繡球哪有那麼大的?那朵花球一個人懷抱都抱不住。但其它方面確實很接近。她發了個視頻給我,是一株臉盆大的繡球花。盡管我看到的還要更大,但我接受了她的說法。

S60516-15501218_com.ss.android.ugc.aweme.png

今天送貓去絕育,心裡好奇,故地重遊,再尋巨型繡球花。印象裡的路段都走遍了,卻沒有找到繡球花的蹤影。公園裡很多人跳交際舞,音樂聲嘈雜著交流聲,和夜晚的靜謐完全不一樣。我感到一陣恍惚,難道其實是在夢中遇到的?

我再次搜索路邊任何球狀的植物。然後我看到了這個。

P20260516-144616.jpg

我無法確定這到底是否是昔日之物,但除此之外再無球形植物。花了好一會我終於接受:我把人工修剪的灌木當成花了。

給用了9年的剃須刀換電池

· 閱讀需 2 分鍾

大學時買的飛利浦PQ190剃須刀,已經9年了,電池垂垂老矣,充滿電用一兩次就刮不動了。想重新買一個,又感覺它好好的扔了挺可惜。我為什麼不能自己換個電池呢?

想到就做,先拆開看看電池什麼樣。結構比我想象中簡單,一塊電路板,一個馬達,一塊7號充電電池。麻煩的是電池是焊在電路板上的,需要用到烙鐵。我有點猶豫,會不會太麻煩了?不過早就想玩焊接了,趁著興奮勁激情下單了電烙鐵和新電池。

第一步是把舊電池拆下來,先用電烙鐵融掉引腳上的錫(圖是換好之後才拍的):

P20260513-153724(1).jpg

錫真的很好融誒,但是把它弄下來好難!剛關掉烙鐵電源,它又“幹”掉了,只能靠烙鐵一點一點蹭下來,我為什麼不買吸錫器@_@

廢了九牛二虎之力,終於清理掉了老錫。又碰到新問題:新電池的電極片比舊電池寬一點!電路板上的卡槽插不進去...事已至此斷無放棄的理由,用小刀慢慢把口子磨大,總算塞進去。

最後一步是重新焊接電極片,防止松動。但此刻我的烙鐵針頭已經因為使用不當有點損壞了,針頭的一部分融不掉錫,用根部一點才成功融化錫絲。

錫絲要很靠近電極片才行!因為錫凝固得很快,前幾次錫都在錫絲上直接凝固成球了=_= 好在經過幾次失敗,最終還是成功焊住了電極片。雖然焊成一坨很醜陋啦(俗稱雞屎焊)

搞定收工!裝上蓋子,擰上螺絲,再戰十年!

P20260513-153823.webp

謹以此文紀念第一次用電烙鐵。

冰室花園

· 閱讀需 1 分鍾

終於把博客從肥大的java後端遷移到了Docusaurus靜態生成,內存佔用從幾百M降到不到10M,很是欣喜。

大學畢業後的幾年博客網站幾乎被我遺忘了,只是堅定地維持著運行。年初換了臺服務器,配置很低,博客網站被我遷移到了家裡的服務器節約寶貴的內存空間。

今年公司開始提供充足的AI報銷額度。AI帶來的生產力提升讓我熱情高漲,開始折騰集群,整理我的家庭服務器,做app,部署各種服務。博客遷移,這個在todo裡躺了幾年的待辦終於被提上日程。

遷移過程出奇的順利,AI幫我導出數據、創建新項目、生成logo,創建服務部署清單,不到半小時便替換掉了原來的Halo服務。此刻正在看假面騎士創騎,有個角色叫作冰室幻德,於是我給新的博客網站取名為:

冰室花園

記一次誤操作刪除800G數據的經歷

· 閱讀需 4 分鍾

前因後果

2021年2月5日,我正在嘗試運行一份示例代碼。該腳本類似這樣:

# 这里有检查$REC_ROOT,但本脚本内并未处理,所以只会输出缺少环境变量$REC_ROOT,但继续执行
./config.sh
if [ ! -d $WAV_ROOT ]; then
echo "Cannot find wav directory $WAV_ROOT"
exit 1
fi

data="$REC_ROOT/data"

# 其他代码

if [ $stage -le 0 ]; then
echo ""
echo "Stage 0: Preparing data"
rm -rf $data/*
local/chime1_prepare_data.sh || exit 1
fi

由於腳本來自知名開源項目,我並沒有仔細審查。另外由於對相關代碼並不熟悉,我也沒有正確配置相關環境變量,所以腳本中的WAV_ROOTREC_ROOT理所當然是未定義的。

我就這麼冒失地執行了腳本,而它在打印兩句警告後並沒有停止執行,所以我認為環境變量並不是必須的,於是放任它繼續執行。由於該腳本執行的是耗時任務,我將控制臺隱藏到後臺,去完成其他任務。

過了十幾分鍾,我收到一個應用程序的崩潰報告,因為相關文件不存在。我疑惑地檢查,發現數百GB的數據已經不翼而飛。此時我才想起那個正在執行的腳本,切換過去後發現它還沒有停止執行,正在瘋狂刪除我的文件。我趕忙殺掉了腳本,但包括由於沒有root權限而刪除失敗的,原本800GB+的數據只剩5.6G。

讓我們來看看發生了什麼。

首先,腳本調用config.sh,檢測到REC_ROOT環境變量不存在並打印警告。

然後腳本繼續執行,if [ ! -d $WAV_ROOT ]; then這裡是在檢測WAV_ROOT是否是一個目錄,如果不是,就退出腳本。按理說我並沒有配置任何環境變量,此處應該退出。但bash腳本神奇地,當WAV_ROOT為空或者不存在時,這個檢測會認為這是一個目錄,從而通過檢測。即:

unset NOT_EXIST
if [ -d $NOT_EXIST ]; then
echo "this is a directory"
fi

上面的腳本是會輸出的。

或許是因為參數為空時bash默認檢測當前目錄,以至於目錄檢測總是通過。

再然後,由於REC_ROOT未定義,$data=/data,然後相當於:

rm -rf /data/*

非常不巧和不幸的是,我將一塊1TB的數據盤掛載在了/data上,於是迎來了降維打擊。該數據盤中有800G+的數據,文件量大於10萬,因此非常耐刪,過了十幾分鍾還給我剩了幾個G。而大量讀寫操作將數據恢復的難度推到了地獄級。

搶救措施

在殺掉腳本之後,我嘗試卸載數據盤,但卸載失敗,提示正忙。情急之下我忘記了可以通過正在運行的進程恢復它們打開的文件,而是想到先關機避免更多的讀寫。關機前發現VSCodium還在運行並且有未關閉的文件,於是搶救出幾個正在編輯的代碼文件。而這成了本次事故中我唯一搶救成功的文件。

之後,通過U盤刻錄的系統修改原系統的配置,取消掉自動掛載數據盤,然後系統啟動後以只讀方式掛載數據盤嘗試恢復數據。

正如前面所說,大量的讀寫操作讓我失去了恢復的機會,嘗試了不少恢復工具,但都沒法掃描出目錄結構,唯一可能有效的方法是通過特殊的文件頭結構進行特徵掃描,但這只能恢復一些特殊格式的文件,而對我最重要的都是純文本數據,至於一些視頻文件,由於尺寸太大數據分散在不同的塊,也是基本沒戲的。

還好,雲端備份讓我不至於一無所有,但還是痛失最近一個月的活動數據和大量不可描述之物。

總結

數據無價,謹慎操作。備份得當,也別太浪。

DOM更新與瀏覽器事件循環

· 閱讀需 5 分鍾

通常我們說DOM更新發生在一個宏任務執行完之後,但並不是每次宏任務執行後都會進行DOM更新。瀏覽器會維護一個相對穩定的幀率,根據硬件條件和頁面性能表現。那麼事件循環如何判斷是否應該更新DOM呢?

先上一波HTML規範指定的事件循環處理章程

  1. 從事件循環的任務隊列中取一個任務隊列taskQueue,這裡的任務隊列指的是宏任務隊列。注意,一個事件循環可能並不只有一個宏任務隊列。具體有哪些以及這裡怎麼選由用戶代理決定。
  2. taskQueue中出隊一個(宏)任務oldestTask,並設置事件循環的當前執行的任務taskQueue
  3. 執行taskQueue
  4. 設置事件循環的當前執行的任務為null。
  5. 執行微任務隊列中的(task)。
  6. 處理DOM更新。

上面的描述省略了一些步驟,完整版參考規範。注意worker環境的事件循環不在本文討論範疇之內。

可以發現DOM更新確實發生在兩個宏任務執行之間,但如果我們深入DOM更新步驟,會發現還有個渲染機會(rendering opportunity的概念。DOM更新時會忽略沒有渲染機會的文檔。

那麼什麼時候一個DOM文檔(準確說是其瀏覽上下文)擁有渲染機會呢?前面說過,瀏覽器會維護一個相對穩定的幀率,例如30fps,那麼1秒內應該最多擁有30次渲染機會。瀏覽器根據這些條件和是否有DOM更新(DOM操作,動畫等),決定文檔是否應該擁有渲染機會相對穩定的意思是,當硬件條件或者頁面性能導致無法維持較高幀率時可能會降幀。雖然這並不是規範規定的,但規範中以此為例,而且主流瀏覽器應該都是這麼實現的。

了解這些有什麼用呢?我們可以更精細地把握自己的代碼。考慮以下代碼:

setTimeout(() => {
//依赖DOM的操作
}, 0);

我們可能會用定時器來等待dom更新後執行一些操作,因為定時器創建宏任務,而DOM會在當前宏任務執行結束後更新(錯誤)。
有趣的是,這往往也不會出大問題。一來一般這時候已經執行了不少代碼,可能正好瀏覽器在當前宏任務執行完後更新了DOM,二來DOM操作往往會導致DOM強制更新(如讀取DOM元素幾何相關的數值等,但不絕對觸發強制更新),也可能會得到想要的結果。
然後結果是,bug若有若無的,全看人品。啊,我測試的時候好好的,演示的時候怎麼就...😅

上面的例子不夠具體,來個更實際的。假設要實現一個DOM元素從遠處飛回來的動畫,我們不用css animation,用transform。
首先我們需要給DOM元素一個初始偏移量,否則起點就是終點,就沒有動畫效果了。然後給它設置transition樣式,並把偏移量還原。

// 获取DOM元素,这里getElement函数未实现
const el = getElement();
// 初始偏移量
el.style.transform = 'translateX(-100px)';
// ...???
el.style.transition = 'transform 100ms linear';
el.style.transform = 'translateX(0)';

為了讓動畫從初始偏移量開始,我們需要在???前後代碼執行之間觸發一次DOM更新。

最簡單的就是觸發DOM強制更新。

// 获取DOM元素,这里getElement函数未实现
const el = getElement();
// 初始偏移量
el.style.transform = 'translateX(-100px)';
el.getBoundingClientRect();
el.style.transition = 'transform 100ms linear';
el.style.transform = 'translateX(0)';

通過getBoundingClientRect()讀取元素的幾何信息,迫使DOM重新計算元素的屬性。但這可能造成額外的運算,帶來性能損失。雖然一般場景下問題不大。

如果我們想將???後面的代碼延遲到下一幀之後執行呢?靠setTimeout()的話,timeout設置小了就回到bug與人品的故事了,設置大了又會產生卡頓影響體驗。這時候我們可以通過一個神奇的API來解決:requestAnimationFrame

// 获取DOM元素,这里getElement函数未实现
const el = getElement();
// 初始偏移量
el.style.transform = 'translateX(-100px)';
requestAnimationFrame(() => {
setTimeout(() => {
el.style.transition = 'transform 100ms linear';
el.style.transform = 'translateX(0)';
}, 0);
});

首先,requestAnimationFrame()的回調會在下一幀繪制之前執行。我的理解是,執行requestAnimationFrame的回調函數列表作為一個宏任務被調度,並且此時該DOM文檔獲得渲染機會,因此回調執行結束後DOM將更新,而當回調函數中設置的定時器任務被執行的時候已經處於新的宏任務中,此時DOM文檔已更新。

注意:對於處理一個瀏覽上下文的requestAnimationFrame回調時的宏任務和下一個宏任務之間,該瀏覽上下文一定獲得會渲染機會是否成立,尚未找到規範證明,僅作參考。

本文鏈接:https://blog.mattuy.top/archives/dom-update-and-browser-event-loop

LeetCode之旅──696.計數二進制子串

· 閱讀需 2 分鍾

題目:給定一個字符串 s,計算具有相同數量0和1的非空(連續)子字符串的數量,並且這些子字符串中的所有0和所有1都是組合在一起的。
重復出現的子串要計算它們出現的次數。

示例:

輸入: "00110011"
輸出: 6
解釋: 有6個子串具有相同數量的連續1和0:“0011”,“01”,“1100”,“10”,“0011” 和 “01”。

請注意,一些重復出現的子串要計算它們出現的次數。

另外,“00110011”不是有效的子串,因為所有的0(和1)沒有組合在一起。

最先想用棧來解決,但發現行不通。然後慢慢發現規律了,我們可以用一個指針對準第一個數字的起點(最先是0),然後用另一個指針對準另一個數字的起點(如示例中第一個1的位置),然後兩個指針一起跑,只要它們對應位置的值不相等,那答案就加1。值相等後必然是其中一個數字跑完了,可以根據和前一個位置對比得出是跑在前面的指針跑完了還是跑在後面的,如果是前面的先跑完了就讓後面的指針直接跑到下一個起點(即前面的指針本次的起點),接著開始下一輪,直到有一個指針到達末尾。

好吧上面一段其實並不用看,我們只需要將相同的數字看作一組,然後遍歷每組的數字個數組成的數組,相鄰元素取最小值,加起來就得到結果了。比如示例的數組為[2, 2, 2, 2],相鄰取最小再加起來就是6。

其實理論上說兩個方法是差不多的,後者像是前者的抽象化。我們刷算法題總是容易下意識的用人類思維思考,然後用計算機去“模擬”,所以經常會遇到將思維轉化為代碼時不流暢和容易出錯。我認為所謂算法思想,就是一種將人類思維抽象成計算機思維的能力