并行化密集型计算

稍早前对 terser 压缩代码进行优化时将执行过程的代码从 Python 迁移到了 Node.js,准确地说是试图用 gulp stream 来批量压缩,用了一个比较老的包(gulp-terser),已经很久不维护了。与原先相比,底层都是 terser,只是为了在 Node.js 的镜像下使用而统一了文件的提取(glob)和批处理过程(Python Pool & subprocess -> gulp stream)。

但这样处理了以后发现压缩处理时长变长了很多,差不多慢了 1 分钟,研究了一下 gulp 的处理机制,发现虽然 gulp 号称流式处理,但文件依然是按个通过 pipe 的,前一个处理完下一个才能进入,但通过 parallel-transform 也能实现同时处理多个文件,可是实际操作中以 parallel-transform 替代 through2 并不能提升处理效率。笔者并未实际深究其原因,大致读了下 parallel-transform 的实现,它依然是围绕着 event-loop 做文章,设想成一个水池,干涸了就加水,满了就停止并等待处理完毕抬高基准线。

笔者甚至使用了 RxJS 来控制并发 …… 但事实证明还是自己太年轻了,这种密集型计算根本无法通过 event-loop 来优化,它和 ajax 或者 IO 操作不一样,处理数据的仍是这个进程,怎么办?那就多进程/多线程!

问题就变得简单了起来,第一步把文件列表拿到,第二步把它们分发到其他进程/线程处理。

首先想到的是 gulp.src 接受的是数组,而 glob 接收的是字符串,实现肯定不一样,事实证明果然相差甚远。想想看还是需要先拿到本着能不造轮子就不造轮子的思路,去网上搜搜代码,首先想到的是已废弃的 gulp-util。一看里面有个 buffer 符合我们的需要,直接抄来,只不过需要注意一下这里有个 cb 需要塞到 fn 的回调里去,这样才能准确得到 task 的总共执行时间。

1
2
3
4
5
6
7
8
9
var end = function(cb) {
this.push(buf);
--- cb();
--- if(fn) {fn(null, buf);
+++ if (fn) {
+++ fn(null, buf).then(cb)
+++ }
}
};

fn 是个耗时的操作,必须再它完成后再 cb

然后按照 terser 和 child_process 的关键词找到了 @bazel/rules_nodejs - terser 这个包,简直是宝藏 …… 一顿大抄特抄,写死了 terser 执行参数,加了个 Promise 的返回值。上 pre 跑了一下,功德圆满!

快了整整 4.5 倍!

为啥这么快?看了下 CPU 数量,我惊呆了!

48 核???

顺便看了下 issue 里也有试图引入 works_thread,不过 child_process 已经足够香了!

不过 web 上毕竟一个 page 只有一个进程,所以要有 Web Worker 来让密集计算在主线程之外运行,而可预计到的是 Web Worker 和 work_thread 的关系就好比 wasm 和 wasi,激动人心的进展!

并行计算大势所趋

程序员应该写出怎样的代码

虽说老板和产品经理都不会太关心代码质量,所求无非是美观可用效率高等等,但作为一个略有追求的程序员在严酷的需求排期和反复推翻修改的生存环境之下,面对前人挖下的坑和自己几个月前寥寥草草写出的 adhoc 代码,总会望洋兴叹一番,痛定思痛想要写出一些拯救苍生(几个月后的自己)的传世代码。

然鹅面对一堆静态动态模板混杂、数个参数可变返回数据可变的异步嵌套接口加上鬼都不认识的 model/view 继承堆积起来意大利面条一般的代码,更别说还有不知哪里引用的全局样式给兴高采烈调完所有组件的你温柔一刀。怎么办?

首先从简单的样式部分着手,CSS Module 或 styled component 完美解决了局部样式的问题,SSR 解决了前后端模板的一致性。这些都是社区提供的通用方案,剩下的基本就是如何组织 model 以及接口了,即前后端数据同步问题。

我们知道 Python 和 JavaScript 都是动态语言,而动态语言最臭名昭著的就是“动态一时爽,重构火葬场”,基本一个数据倒上那么三四次手之后就变得亲妈不认了。类型是一定要加的,我们用 TypeScript!

https://juejin.im/post/5d8efeace51d45782b0c1bd6

具体的好处前面已经提到过了,这里想说明的是类型定义不仅保证了代码逻辑的正确性,也给予阅读时良好的体验,有了类型以后相当一部分关于变量的说明都可以挪到 type 和 interface 上,代码更容易实现“自解释”。其实 ES Module 很大程度上已经完善了代码的引用逻辑,但相比之下类型定义具有更强的约束(字段级别)和更灵活的配置(interface 鸭子类型),所以都 0202 年了,还不上 TypeScript 真的都没法说自己是工程化的前端项目。

但 TypeScript 只是解决了前端的代码一致性,JavaScript 总还是要在 web 这个容器内运行的,目前边缘计算还不够成熟,以服务器为数据源和计算存储中心的 C/S 架构仍是未来一段时间的主流,所以接口相对于前端的重要性是不言而喻的(插一句,往后想想,与设备的连接接口以后会不会成为前端的新方向呢?这就成了边缘计算)。而对于 HTTP 请求而言,RESTful 是曾经统治一时的事实标准,非常 CRUD,但也非常不灵活。

我们需要的是一种组织更加灵活、分层结构、高度复用、最好实现“自文档”的接口,这就是 GraphQL。虽然它也存在嵌套查询时会拖慢数据库,参数传递不清晰等毛病,但瑕不掩瑜,它确实是目前前端对于结构化数据查询最好的方式,更为出众的是它支持自定义返回字段,不多不少刚刚好是前端想要的那些,这简直太香了。

示例查询,用户 -> 作品 -> 评论/收藏用户/章节列表 -> 自定义格式上架时间

如果不考虑性能和异步并发,这些已经足以一个前端掌控 95% 的开发任务了,但往往那 5% 的事儿占据了 95% 的开发时间,后面会写写对于 Web Assembly 和 Reactive Programming 的理解,长远来看,当前围绕 html 进行的框架或者语言升级将告一段落,往后会转入多语言及范式涌入 web 开发领域碰撞并融合,浏览器正式成为继 iOS 和 Android 之后的第三大移动操作平台,更为可贵的是 web 的普适性使它几乎没有什么前置使用成本。

过度包装

疫情期间每天都是自己做饭吃,隔两三天就要买一次菜,每次买菜光是拆包装盒就要耗费非常多的心力,更别提清洗食材和锅碗,切菜淘米调配料,蒸炒炖炸等工序了,难怪说疫情期间分手的情侣、离婚的夫妻比比皆是,敢情大家平时都是装作很会生活的样子,一旦没了外卖快递都退回到了饮毛汝血的状态。

正如做饭 1 小时,吃饭 10 分钟,洗碗半小时所说的那样,吃这样一个动作在当前都会有无穷的副作用,可以说现代都市生活真的是建立在非常完善的 O2O 服务行业之下的,要吃饭线上下个单线下送来,要保洁自如下个单线下保洁员上门,要 ml 都有 165/90 32C 的家政服务人员上门服务。人的生活变成了只需处理工作,其他时间都可以用钱买到,这样 WFH 所带来的工作时长延长对社畜的身心打击几乎到了极致。扯远了。

网上买菜或者超市买菜,相比于菜市场买菜最大的区别是包装。垃圾桶里丢掉的绝大部分是塑料包装而不是切掉的边角料菜或者剩饭剩菜。就像软件开发里,动辄几十上百层的封装之下,真正工作的是那么一两行代码。

  1. public
  2. js
  3. submit
  4. works
  5. manage
  6. components
  7. header-actions
  8. PriceActions
  9. PriceActions.tsx -> StartPromotionButton.tsx -> StartPromotionForm.tsx

一个页面上小小的组件,被封装在 9 层目录之下,同目录平级之间还有嵌套关系。当然组件化分治思想也是 React 一直火到现在的根本原因,但有时也会想想这种扁平化的目录结构是否真的合适项目的发展,或者说在业务需求猛增前提下如何科学组织目录结构以便修改维护也是门学问了,当然最好的方式是:不组织。根据实际发展来让文件自行组织,见 destiny

拉扯这么多包装的弊端,但包装实际上是一项很重要的工序,因为不包装带来的风险,往往是看不见的:
小吕去年写了个活动页的接口,因为是第一次办这种活动,小吕本着以产品需求为约束的理念写好了无届数限制的 API 接口,然后前端同学以此接口封装了一个组件给页面加了上去。转眼间一年过去了,活动办了第二届,产品要求有新功能,小吕又哼哧哼哧写了一个第二届活动的 API,但这时候出分歧了:前端觉得接口应该稳定,如果每做一届都要回溯一遍做过的接口映射,太累且容易出错;而后端觉得分离 API 有助于更清晰地业务逻辑。大家说的都没错,怎么办呢?包装一下接口吧!

原俩接口实现,红线为本次修改
Adapter 模式

可以看出是适当的包装有助于分清业务界限,明确各端职责是非常值得提倡的。

但为什么实践中经常分不清这些呢?因为 Python 本质是一个胶水语言,更多起黏合剂作用,而 JavaScript 更想火柴棍,可以飞速搭原型,但两者都不是拥有持续集成性和可维护性的代表。个人以为类型系统以及所有权(或者说内存锁)是一个合格的工业级语言必备的,一个保障 AOT 安全,一个保障 JIT 安全,任何业务逻辑都是对这两个基本原则的封装。