所有由罗佳(博主)发布的文章

[WebAssembly]初学笔记 Pthreads多线程

如果有看不明白的地方请先看前置说明文章

对于有高性能要求的可并发任务来说,在程序一开始设计的时候就要把多线程协作考虑进去,因此介绍完了一些emscripten的基本操作之后,我立刻就来到了多线程的章节。

Emscripten提供了两种实现多线程的方式:

  • POSIX Threads (Pthreads) API (以下简称pthreads)
  • Wasm Workers API (以下简称wasmworkers)(在这里继续再批判一次官方这破API文档竟然不写每个函数的用法,其它东西扯了一堆)

我参阅了一下wasmworkers的文档,以上两种API基本能实现相同的功能,区别在于它的wasmworkers是直接根据js原生worker原本的API实现的,因此输出的编译结果会比较小,而pthreads实现了完整的pthreads api,所以输出的编译结果会比较大。由于考虑程序的可移植性,所以这里我只学习使用pthreads实现多线程,如果对pthreads不熟悉的话可以看一看这个Rookie-Note上的学习文档简单了解一下,还是挺简单的,我也刚学会才来写了这篇笔记。

本篇笔记不会对pthreads api如何使用进行全面的说明,因为内容比较多且不是专属于wasm的内容,在代码编写上也和普通的 C pthread程序没有什么区别,可以看我上面贴的菜鸟笔记链接去学习,这里主要就说明一下emscripten中pthread的实现方式和举个例子,顺便贴上emscripten的pthread指导链接

首先要注意

  • 使用以上两种多线程API的时候都不可以和 `-sSINGLE_FILE` 编译参数搭配使用,也就是使用多线程就无法把输出文件全部打包到一个js文件里了。
  • 要使用pthreads需要浏览器开启SharedArrayBuffer支持(在目前的浏览器中由于存在安全原因默认是禁用的),在nodejs中可以直接使用。wasmworkers我没有测试,但应该也是一样的。
  • 编译参数:要使用pthreads库,需要在编译时添加 `-pthread` 参数。

继续阅读[WebAssembly]初学笔记 Pthreads多线程

[WebAssembly]初学笔记 使用Embind在Javascript与C++之间交互

如果有看不明白的地方请先看前置说明文章

Embind是emscripten提供的又一种在js和c++之间的交互方案,其提供更加丰富的交互方式,不止是前面的笔记中介绍的那种简单的函数调用。

Embind库API参考文档地址:https://emscripten.org/docs/api_reference/bind.h.html

官方指导文档地址:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html,下面简单概括一下这个库包含的功能。

1. 在js环境中绑定c++中的(绑定指的是把另一个语言中的概念映射为当前语言中类似的概念):

2. 在c++环境中:使用`val`类操作js中的任意对象。 继续阅读[WebAssembly]初学笔记 使用Embind在Javascript与C++之间交互

[WebAssembly]初学笔记 在C++中嵌入Javascript代码

如果有看不明白的地方请先看前置说明文章

前一篇笔记写了如何在js中调用c++的函数,要在c++里执行js代码也有几种方法,另外和从js到c++的交互只能调用函数和操作内存不同,c++里可以编写完整的js代码并获得结果。

这里依然先附上官方的参考:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html


使用emscripten_run_script直接执行代码

这是emscripten提供的执行js代码的方法,此方法无法获取js代码的运行结果(类似于返回值),可以获取返回值的版本在下文。这个方案的内部实现方式是把这个代码字符串扔外面的js环境用eval()来执行。

在test.cpp中编写如下代码:

#include <emscripten.h>

int main() {
	emscripten_run_script(
		"console.log('岂因祸福避趋之');\n"
		"console.log('6');");
	return 0;
}

然后执行命令编译:emcc -sMODULARIZE=1 -sASSERTIONS -sEXPORT_ES6 -o dist/test.mjs test.cpp

继续阅读[WebAssembly]初学笔记 在C++中嵌入Javascript代码

[WebAssembly]初学笔记 从Javascript调用C++函数

如果有看不明白的地方请先看前置说明文章

要在js中使用c++编写的函数或者在c++中运行js代码,则需要有一些方法打通这两个环境,官方已经写了一个列表列举出了所有的方法,可以参考:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html

在此我会对部分方法简单进行举例。

上一篇笔记说到如果不需要自动执行main,可以不写main函数,此时我们需要使用wasm中编写的函数就需要手动设置需要导出的函数。


函数导出

要从C++中导出函数给js环境使用,有两种方法:一种不用改动代码,需要在编译参数上进行指定,另一种需要在代码中添加宏来指定。

这两种方法的使用场景我认为是这样:如果有一份源码不是你写的,或者说就是引用别处的一个库,那么我们去修改其源码肯定不便于后期维护,因此在编译参数中指定导出的方法最稳妥,但如果要编译的源码就是你专门为了这个wasm项目写的,那么使用宏指定可以让编译参数更加简洁,在代码中也可以更清晰地看出导出了哪些函数。

方法一:需要在编译时添加参数指明要导出的函数名称:

-sEXPORTED_FUNCTIONS=_func1,_func2,_func3,其中func1,func2,func3是在c++中你定义的函数名称,作为导出函数时,需要在其名称前面添加一个下划线,然后这些函数就会被添加给wasm的实例对象。

现在将test.cpp的内容修改为下:

#include <iostream>
extern "C" { // 由于C++导出方法会对方法进行重命名,这是用来指定以C形式导出方法

//定义一个无返回值函数,参数为一个int
void poi1(const int val) {
	std::cout << "poi1: " << val << std::endl;//输出val的值
}
//定义一个返回整数的函数,参数为两个int
int poi2(const int val, const int val2) {
	std::cout << "poi2: " << val << " " << val2 << std::endl;//输出两个val的值
	return 114514;
}
//定义一个无返回值函数,参数为一个bool
void boolTest(bool val) {
	std::cout << "bool: " << val << std::endl;
}

}

然后执行命令编译:emcc -sMODULARIZE=1 -sASSERTIONS -sEXPORTED_FUNCTIONS=_poi1,_poi2,_boolTest -o dist/test.mjs test.cpp

继续阅读[WebAssembly]初学笔记 从Javascript调用C++函数

[WebAssembly]初学笔记 安装环境和尝试编译

以前也尝试过学WASM,但由于当时没有应用场景所以安装完环境就弃坑了,这次来好好学习一下用法。这篇笔记是边学边写的,如果有错误或者需要补充的地方请留言。

WASM通常需要使用其他高级语言编写后编译为wasm二进制文件交给运行时去运行,但如果你的头够铁,也可以尝试手写wasm指令,或者通过wasm的文本格式理解它的底层运行原理。

本笔记在Windows平台上使用C++编写源码编译为wasm,所以需要安装emscripten工具包。除了C++以外,Rust也是一个推荐选项,而其它语言目前还没有非常完备的支持。

安装Emscripten

这是用于把C/C++编译到wasm的工具包。

Emscripten的项目地址在:https://github.com/emscripten-core/emscripten

文档地址在:https://emscripten.org/index.html

安装方式官方手册在:https://emscripten.org/docs/getting_started/downloads.html,这里我简单介绍一下,但还以官方手册为准,我不会同步更新。

首先把emscripten仓库克隆到任意位置,然后进入该目录

git clone --depth 1 https://github.com/emscripten-core/emsdk.git

cd emsdk

然后依次执行以下命令进行安装

emsdk.bat install latest
emsdk.bat activate latest --permanent

上面的` –permanent`参数是为了让emsdk的命令在全局可用,否则你需要在每个命令环境中都执行一次activate。执行完了之后可以关闭当前的命令窗口,然后找个地方开始准备写我们的代码了。 继续阅读[WebAssembly]初学笔记 安装环境和尝试编译

[MySQL]JSON数组取对称差集函数

写了个函数,做个记录。作用是扣掉两个数组相同的部分,只保留不同的部分,也就是对称差集。

CREATE FUNCTION `array_diff`(`arr1` json, `arr2` json) RETURNS json
    NO SQL
    DETERMINISTIC
BEGIN
	RETURN (
		WITH 
		A AS (SELECT el FROM JSON_TABLE(arr1, '$[*]' COLUMNS(el INT PATH '$')) T),
		B AS (SELECT el FROM JSON_TABLE(arr2, '$[*]' COLUMNS(el INT PATH '$')) T)
		SELECT JSON_ARRAYAGG(el) FROM (
			SELECT el FROM A WHERE A.el NOT IN (SELECT el FROM B)
			UNION
			SELECT el FROM B WHERE B.el NOT IN (SELECT el FROM A)
		) AS T
	);
END

调用

select array_diff('[1,2,3]','[2,3,4]')

结果

[1, 4]

 

网页视频截图、录制GIF、调整时间脚本

源码地址在:https://github.com/JiaJiaJiang/jia-webvideo-tools

我也写了一个油猴脚本来引用打包好的项目输出文件:https://gist.github.com/JiaJiaJiang/736f5a90b55b815a0e9eb6463dd09061,需要注意,引用的文件位于GitHub,所以需要过墙才可以正常加载。

主要功能均通过Alt+Shift+右键调出的工具条操作,鼠标悬浮在各个项目上会有说明。其它快捷功能见脚本描述或者Github的readme。

截图效果:

[SQLite]导入自己写的扩展出现”SqliteError: 找不到指定的程序”

自己写了个sqlite c++扩展,然后编译之后载入出现了标题上的错误,如果是英文的话应该是”SqliteError: The specified module could not be found”。

出现这个错误并不是sqlite没找到对应的dll文件,如果是没找到文件的话会提示“找不到指定的模块”。

这个情况是sqlite载入dll之后找不到程序入口,但我在 load_extension 和 c++ 代码中的入口名称明明就是一致的,于是我用 `dumpbin /exports dll文件` 查看dll到底导出了什么入口,发现导出了这么个东西:

_Z22我的入口名P7sqlite3PPcPK20sqlite3_api_routines

我把这一串名字写到 load_extension 中之后终于可以正确导入了,但我肯定不能让它一直生成这样不确定的名字,所以开始找找办法让它只导出我指定的名字。

然后发现这是我用了c++的原因,要解决dll导出的入口名和代码里写的不一致的问题,只要在 `__declspec(dllexport)` 前面加上 `extern “C” ` 即可,完整的是

extern "C" __declspec(dllexport)

这样改完再编译,使用dumpbin查看导出的函数名就已经正常了。

让docker构建nodejs应用时使用npm缓存加速安装

我的node应用在原本的部署脚本下,每次部署都要十几分钟,而我的项目又不适合多阶段构建,于是想了各种办法让它使用镜像layer缓存一部分安装过程,但是多多少少都有点问题。

看来想要不出问题的话还是要在部署的时候让安装过程完整跑一次,这样要缩短部署时间就只能尽量利用npm缓存。

第一步:挂个外部缓存

我本来以为dockerfile构建过程中是没法像容器那样绑定一个volume来让过程文件持久化的,但是昨天我才发现其实是有的,就是要更新一下docker,这是Buildkit特性的一部分,这个特性在18.09版本之后的docker才有,总之尽量把docker升级到最新版就可以用了。

使用方法很简单,如下Dockerfile:

FROM node:18.18.1-buster-slim
RUN apt update &&\
	apt install -y git openssh-client python3 curl
COPY ./deploy /deploy
COPY ./app/ /app
RUN --mount=type=cache,target=/root/.npm \   #在RUN命令的开头这样写来挂一个缓存目录,这个mount参数需要在每个要使用此缓存的命令里都写一遍
	sh /deploy/install.sh   #然后执行你的安装脚本
WORKDIR "/deploy"
ENTRYPOINT ["/bin/sh"]
CMD [ "./start.sh" ]
EXPOSE 80

这样在这个dockerfile构建过程中就会把构建容器的`/root/.npm`映射到通用的docker缓存里,而且我测试下来如果在其它镜像里也挂载同样的缓存,那么其它镜像构建的时候也可以使用该缓存,但我其它镜像的FROM镜像都是相同的,不知道如果来源镜像变了是否会影响缓存挂载。

注意:~/.npm 是linux下npm默认的缓存目录,但默认缓存目录是可以更改的,如果你的项目里修改了npm的默认缓存目录地址,那么这里也要一起改。或者如果你的平台比较特殊,npm的默认目录本来就不在这里,那么也要进行对应的修改。

除了挂载缓存(type=cache)以外,还可以绑定外部文件或目录(type=bind),相关资料在这里https://docs.docker.com/build/guide/mounts/,不过这个文档有点迷惑,没写source和target哪个是里面哪个是外面的,我在source里面写外部路径它给我报文件不存在,然后我就直接用cache了,没再继续尝试。

如果你觉得构建过程缓存的文件有问题,或者单纯想清除这些缓存,可以根据这里的方法,执行以下命令:

docker builder prune --filter type=exec.cachemount

如果没有效果,可以尝试去掉–filter及其参数。

第二步:让npm优先使用缓存(可选)

npm在安装依赖时,即使本地有缓存,也会向服务器发起请求检查每个本地的缓存有没有过期,这个过程也很漫长。其实一般即使缓存过期了也问题不大,因为正常来说同一个版本号的包其内容是不会变的,所以可以让npm优先使用本地缓存,跳过检查其在线状态,这样可以大幅减少安装时间。

方法很简单,只要给安装命令加个`–prefer-offline`参数:

npm i --prefer-offline
#也可以再加个 --verbose 参数确认是否真的使用了缓存
npm i --prefer-offline --verbose

这样折腾完了之后,应用的后续部署时间在依赖没有改变的情况下从原来的十几分钟缩短到了一分多钟,堪称火箭级加速,总算解决了一个困扰我一年多的问题。

[docker]GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name org.freedesktop.secrets was not provided by any .service files

这问题很奇怪,我在这部署了几个镜像都好好的,到其中一个的时候突然就报出了这样的错误:

failed to solve: node:18.18.1-buster-slim: error getting credentials - err: exit status 1, out: `GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name org.freedesktop.secrets was not provided by any .service files`

然后查了下说跑这个命令安装依赖`apt install gnome-keyring`就好了,我试了一下确实解决了问题,但为什么好好的突然就不行了依然是个谜。

[docker]ERROR: Service ‘***’ failed to build: the –mount option requires BuildKit.

我在dockerfile中用了个–mount参数,结果一开始一直报`ERROR: Dockerfile parse error line 8: Unknown flag: mount`,然后我发现是我的docker版本太低了,于是升级了docker之后发现又变了个错误:

[docker]ERROR: Service '***' failed to build: the --mount option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled

我明明已经设置了两个环境变量

export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1

又研究了一会儿我发现原因出在我用的程序上,我执行的是docker-compose,这个版本还是1.17.1,但docker还内置了一个compose,它的版本是2.18.1,于是直接把命令换成docker compose就好了。

[nodejs]同时使用devDependencies和dependencies以处理本地依赖链接

我做了一个自用的工具包,但我想把其中一部分独立出来放到其它项目里用,所以我把它们放在一个命名空间里发布了出来。

于是就出现了一个问题,本来这些包是属于一整个项目,其中的模块存在一些相互依赖,既然要分开发布那么package.json里依赖模块的目标就不能再写原来的本地路径了,需要改成发布的版本号或者项目地址。

本地开发时依赖路径填写本地路径,执行npm i会帮你直接创建一个目录链接到目标目录上,这样你在目标依赖里的改动就不需要发布或者复制就可以直接在当前项目里测试,但如果需要把依赖目标改成模块的发布版本号的话,即使手动把node_module中安装的模块删除手动创建目录链接,执行npm命令在有些情况下也会把已经创建的链接删除并重新从npm下载发布版本,这样会导致开发时不知不觉使用错误的依赖。

刚刚我测试了一下,发现一个模块可以同时出现在package.jsondevDependenciesdependencies里,而且默认情况下npm会优先找devDependencies里模块的目标是否有效,有的话就会选用这个安装,因此要解决我上面遇到的问题,其实只要把需要本地开发的发布模块同时写在这两个字段里,在devDependencies里的目标写本地路径,在dependencies里的目标写发布版本即可,这样在本地存在目标路径时npm就会优先帮你创建路径,而不是从npm下载发布版。参考如下:

{
	"devDependencies": {
		"@jialibs/utils": "file:../Utils"
	},
	"dependencies": {
		"@jialibs/utils": "^1"
	}
}

就是有两点要注意,如果被依赖的模块版本变了,要注意同步修改这个里面依赖的版本,否则其它地方从npm下载就会安装老版本的依赖,或者另一个解决方案就是像我这样直接一个大版本号或者写到次版本,这样只要大版本或次版本没变,那么别处就始终会下载最新版(已经安装过的需要清除package-lock.json,否则npm还是会按照lock里的版本安装)。

另一点要注意的是如果在生产环境为npm i命令显式添加了--omit=dev这样的参数的话,会导致在devDependencies里出现过的包都不会被安装,这种情况可以尝试把本地路径的包放到`peerDependencies`中。

【佳佳拆解】雅马哈YAS-107音响

今天为了检查音响后面的低音炮接口是不是被我插坏了,顺便确认一下这几年有没有什么奇怪的小动物住在里面,所以拆开来检查一下。

所有螺丝都拧下来之后发现还是打不开,找了个视频看了一下原来是周围有一点点不知道是胶还是减震泡棉的东西,而且整个卡得很严实,不要指望用手掰开来。我用一个圆润的扁撬棍把周围一点点撬开后最后才打开了。

继续阅读【佳佳拆解】雅马哈YAS-107音响

QQNT会每月创建重复资源文件

QQNT是去年腾讯放出的新架构QQ客户端,我在去年12月更新到了这个新版,已经用了3个月,今天发现了这好像有个问题。
这是一个关于接收的图片和视频资源产生重复文件的问题。

原来的PC QQ是把接收到的所有图片放在一个目录中,所有视频放在一个目录中,分别按照QQ自己的hash规则命名,当客户端接收到消息,需要加载图片或视频时将会先按照资源的hash在存放资源文件的目录中查找,如果没有就会从服务器下载一份。这是很河里的做法。

新版的QQNT在这一套接收资源文件的流程上基本上差不多,但有一个地方不同:它在每个类型的资源目录中又以“年-月”的格式加了一层目录,这个“年月”目前看下来指的是附件资源所在消息的发送年月(我这甚至出现了2019-06的资源目录)。这将导致每个月的消息附件资源(图片,表情,视频)不共享,上个月消息中接收过的表情图片在这个月对于QQ来说就不存在了,要重新下载一份。对于缩略图来说也一样,因为缩略图目录也在每个月的目录中单独有一个。

QQNT存放这些资源的路径是”C:\Users\用户名\Documents\Tencent Files\QQ号\nt_qq\nt_data”,其中”Emoji”里面是表情图,”Pic”里面是表情图以外的图片,”Video”是接收到的任何视频,可以发现它们里面全都以年月的子目录对资源进行分隔,我用Everything直接对Emoji目录查找,按文件大小排序,就已经发现了大量完全相同的表情文件出现在不同月份的目录里。

以下是在Everything中搜索到分别存在于1月和2月目录的相同表情文件




这样重复的表情文件和图片文件还有很多,随着时间推移,这种每个月不共用附件资源的特性根据接收消息数量的不同可能会导致空间占用快速增长,每个月都产生大量的重复文件(已确认每个月出现的相同文件都是单独的文件,并不是硬链接)。

目前不理解为什么要这么做,如果是为了避免在一个目录中存在大量文件导致文件系统操作效率降低的话,按照其”avatar”目录中的做法就很好,直接按文件hash的开头来划分子目录即可。
如果QQNT之后也坚持以这种方式存放文件,那用户就要注意定期清理本地重复文件。至于重复下载对服务器资源造成的浪费,那反正也不关用户的事🤔。

继续阅读QQNT会每月创建重复资源文件