早年买了Nikon D7200,放了很久没怎么用,上次去中山带上了单反,感觉拍照效果很好。这一次学会了手动对焦。于是买了个近摄镜,想体验一下微距摄影,拍一下花花草草和昆虫蚂蚁的细节。
上面这些照片都是配合近摄镜手动对焦拍摄的。
早年买了Nikon D7200,放了很久没怎么用,上次去中山带上了单反,感觉拍照效果很好。这一次学会了手动对焦。于是买了个近摄镜,想体验一下微距摄影,拍一下花花草草和昆虫蚂蚁的细节。
上面这些照片都是配合近摄镜手动对焦拍摄的。
最近用rust写的日志上报agent趋近完善,意味着一个练习rust的小项目结束了。于是便找了个新的小项目,用rust代码编译出wasm,在浏览器端实现图片缩放、转码。决定做前端转码是出于两方面原因,第一是想体验一下rust-webassembly,第二是博客的管理后台上传图片能力有待优化,无法直接上传单反拍出来的图片,因为单反照都是十几兆以上大小,我的云服务器只有1M带宽,上传超时,就算我能忍受超时,也无法忍受大文件后端转码压缩时io满负载直接卡死服务器的情况。于是便有了这次wasm体验。
首先,如果你已经入门了rust,能用rust写代码了,那么用rust实现wasm将会是一种非常好的体验。因为rust的wasm全套工具齐全,你可以直接在rust项目中编译出npm包,编译出来的结果可以直接上传到npm仓库。这里简单介绍一下基于rust的wasm包开发过程。
首先创建rust的包项目,注意不是可执行文件。
cargo new wtools --lib
然后,修改Cargo.toml
文件,定义包类型
[package]
name = "wtools"
version = "0.1.6"
edition = "2021"
description = "wasm tools"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"] # cdylib 是wasm库, rlib 是常规rust库
[profile.release]
lto = true
opt-level = 'z'
[dependencies]
注意lib
下的crate-type字段要定义为cdylib
,只有这种包才能编译为wasm,然后还有一个选项需要注意profile.release
下的lto=true
和opt-level = 'z'
这两个选项设置后,可以在编译的时候讲wasm压缩到最小大小,以减小wasm文件在网络中分发的大小。当然,缩减wasm还有个工具,叫做wasm-opt
,但是我具体实测之后发现,只要设置了上面的lto
和opt-level
选项,这个工具能缩减的大小非常有限,有时候甚至无法再进一步缩减了。
安装工具。这里编译wasm
报并不是用原生的cargo,而是使用一个叫做wasm-pack
的工具,它的优点是,可以直接编译出npm包。安装
cargo install wasm-pack
编译
wasm-pack build --scope duguying
上传npm包
cd pkg
npm publish --access=public
整个开发的过程就是如上的那些。下面简单介绍一下代码。首先,我们这个rust项目的目标是编译为wasm在浏览器上运行。这里就免不了js与rust之间进行数据传递,以及rust里操作浏览器中的各种对象和元素。介绍两个rust包,第一个js-sys
,用于js与rust之间进行数据传递的,在这个包里能找到js中的数据类型对应的类型;第二个web-sys
,用于浏览器对象与rust之间进行数据传递的,在这个包里有对应浏览器中的各种对象。
比如,最常见的浏览器日志打印console.log
,在web-sys
中能找到console
对象,详情可以查看文档。在我的rust包中简单的包装了一下
extern crate wasm_bindgen;
extern crate web_sys;
use wasm_bindgen::prelude::*;
#[macro_export]
macro_rules! console_log {
($($t:tt)*) => (web_sys::console::log(&js_sys::Array::of1(&JsValue::from(
format_args!($($t)*).to_string()
))))
}
#[wasm_bindgen]
pub fn greet(name: &str) {
console_log!("Hello, {}!", name);
}
这样就可以在其他地方用console_log
来调用了,比如
console_log!("load img failed, err: {:?}", error);
我需要进行图片处理,所以用到了image
这个包,这个包支持缩放图片resize
、旋转图片rotate
以及翻转图片flipv
等。我主要用到缩放和旋转。另外有一点需要注意的是,需要导出到js的结构体和方法函数等,需要添加#[wasm_bindgen]
注解。这个注解是在wasm_bindgen
这个包中定义的,这个也是rust编译为wasm的核心包,具体可以查看文档。因为我发现单反上拍摄的照片通常会根据拍照者持相机的角度有一个旋转参数,而这个参数,它是存到了照片的exif信息中,但是他的照片数据实际存储是按照相机原始的方向存储的,所以,竖着拍摄的照片在上传到服务器之后会发现照片是横着的,需要旋转90度。所以在这里我还用到了kamadak-exif
这个包,来读取照片的exif信息,从而获取旋转参数,然后根据旋转参数调用rotate
对照片进行旋转来修正照片方向。图片处理的代码如下
extern crate wasm_bindgen;
use exif::{Error, Exif, In, Tag};
use image::{imageops::FilterType, DynamicImage, EncodableLayout, ImageFormat};
use js_sys::Uint8Array;
use std::io::{Cursor, Read, Seek, SeekFrom};
use wasm_bindgen::prelude::*;
use crate::console_log;
#[wasm_bindgen]
pub struct Img {
img: DynamicImage,
img_format: ImageFormat,
exif: Result<Exif, Error>,
orientation: u32,
}
#[wasm_bindgen]
impl Img {
#[wasm_bindgen(constructor)]
pub fn new(img: &[u8], mime: &str) -> Img {
let exifreader = exif::Reader::new();
let (img_data, img_format) = Img::load_image_from_array(img, mime.to_string());
let mut c = Cursor::new(Vec::from(img));
let exif = exifreader.read_from_container(&mut c);
let mut image = Img {
img: img_data,
img_format: img_format,
exif: exif,
orientation: 0,
};
image.get_orietation();
image.fix_orietation();
image
}
fn load_image_from_array(_array: &[u8], mime: String) -> (DynamicImage, ImageFormat) {
let img_format = ImageFormat::from_mime_type(mime).unwrap();
let img = match image::load_from_memory_with_format(_array, img_format) {
Ok(img) => img,
Err(error) => {
console_log!("load img failed, err: {:?}", error);
panic!("{:?}", error)
}
};
return (img, img_format);
}
fn get_orietation(&mut self) {
match &self.exif {
Ok(exif) => {
let r = exif.get_field(Tag::Orientation, In::PRIMARY);
match r {
Some(oriet) => {
self.orientation = oriet.value.get_uint(0).unwrap();
}
None => {}
}
console_log!("orientation: {:?}", r.unwrap());
}
Err(_error) => {}
};
}
fn fix_orietation(&mut self) {
match self.orientation {
8 => self.img = self.img.rotate270(),
3 => self.img = self.img.rotate180(),
6 => self.img = self.img.rotate90(),
_ => {}
}
}
fn image_to_uint8_array(&self, img: DynamicImage) -> Uint8Array {
// 创建一个内存空间
let mut c = Cursor::new(Vec::new());
match img.write_to(&mut c, self.img_format) {
Ok(c) => c,
Err(error) => {
panic!(
"There was a problem writing the resulting buffer: {:?}",
error
)
}
};
c.seek(SeekFrom::Start(0)).unwrap();
let mut out = Vec::new();
// 从内存读取数据
c.read_to_end(&mut out).unwrap();
let v = out.as_bytes();
Uint8Array::from(v)
}
pub fn get_width(&self) -> u32 {
return self.img.width();
}
pub fn get_height(&self) -> u32 {
return self.img.height();
}
pub fn grayscale(&self) -> Uint8Array {
let img = self.img.grayscale();
self.image_to_uint8_array(img)
}
pub fn scale(&self, width: u32, height: u32) -> Uint8Array {
let img = self.img.resize(width, height, FilterType::Triangle);
self.image_to_uint8_array(img)
}
pub fn rotate90(&self) -> Uint8Array {
let img = self.img.rotate90();
self.image_to_uint8_array(img)
}
pub fn rotate180(&self) -> Uint8Array {
let img = self.img.rotate180();
self.image_to_uint8_array(img)
}
pub fn rotate270(&self) -> Uint8Array {
let img = self.img.rotate270();
self.image_to_uint8_array(img)
}
pub fn flipv(&self) -> Uint8Array {
let img = self.img.flipv();
self.image_to_uint8_array(img)
}
pub fn fliph(&self) -> Uint8Array {
let img = self.img.fliph();
self.image_to_uint8_array(img)
}
}
编译成功打包上传npm仓库之后,在前端项目中使用有一点需要注意,像这种基于wasm的npm包并不能像常规的npm包那样直接import引入,而是需要异步引入,这种写法非常不优雅,如下
/**
* @description 全局注册md5工具
*/
async function waitwasm () {
const { Crypt, Img } = await import('@duguying/wtools')
Vue.prototype.$md5 = (content) => {
let crypt = new Crypt()
let out = crypt.md5(content)
crypt.free()
return out
}
Vue.prototype.$scale_img = (file) => {
return new Promise(function (resolve, reject) {
let reader = new FileReader()
reader.readAsArrayBuffer(file)
reader.onload = function () {
let data = new Uint8Array(this.result)
console.log('data:', data)
let kit = new Img(data, file.type)
console.log(kit)
let w = kit.get_width()
let h = kit.get_width()
console.log('wh:', w, h)
if (w > 2000) {
w = 2000
h = h / w * 2000
} else {
resolve(file)
return
}
let out = kit.scale(w, h)
resolve(new Blob([out.buffer], { type: file.type }))
}
})
}
}
(async () => {
waitwasm()
})()
他本身是一个异步引入,但是需要等它引入完毕之后,才能调用其中的方法,否则就会报错,所以,这里只好同步阻塞,等他引入完毕了。
rust中,可以通过core_affinity
这个crate对线程进行核绑定。但是绑核过程中发现一个问题。针对主线程绑核,若是主线程绑核后,创建子线程,在该子线程种尝试绑核会发现只有一个核可以。所以,使用这个库如果需要对主线程进行绑核,需要在所有子线程创建完毕之后进行。
绑核的函数如下
static CORE_IDS: Lazy<Vec<CoreId>> =
Lazy::new(|| core_affinity::get_core_ids().unwrap()); // 尝试过给CORE_IDS 加锁,也是一样
fn bind_core(id: usize) {
let mut selected_id = CORE_IDS[0];
for core_id in CORE_IDS.clone() {
println!("core {:?}, bind to: {:?}", selected_id, id);
if core_id.id == id {
selected_id = core_id;
break;
}
}
core_affinity::set_for_current(selected_id);
}
在主线程中绑核,且主线程的绑核在创建子线程之前,发现,第一次遍历核,核有8个,成功绑定到指定的7号核;第二次子线程中绑核,遍历核,核有1个,为7号核,所以只能绑定到7号核,这样就与主线程同核了。
core_id num: 8, cores: [CoreId { id: 0 }, CoreId { id: 1 }, CoreId { id: 2 }, CoreId { id: 3 }, CoreId { id: 4 }, CoreId { id: 5 }, CoreId { id: 6 }, CoreId { id: 7 }]
print core_id: CoreId { id: 0 }, bind id: 7
print core_id: CoreId { id: 1 }, bind id: 7
print core_id: CoreId { id: 2 }, bind id: 7
print core_id: CoreId { id: 3 }, bind id: 7
print core_id: CoreId { id: 4 }, bind id: 7
print core_id: CoreId { id: 5 }, bind id: 7
print core_id: CoreId { id: 6 }, bind id: 7
print core_id: CoreId { id: 7 }, bind id: 7
core_id num: 1, cores: [CoreId { id: 7 }]
print core_id: CoreId { id: 7 }, bind id: 6
改为,主线程绑核在创建子线程之后,如下
fn main() {
let cfg = config::cfg::get_config();
let filename = cfg.las.as_ref().unwrap().access_log.as_ref().unwrap();
let mut watcher = LogWatcher::register(filename.to_string()).unwrap();
let poly: Poly = Poly::new(); // 此处调用会创建子线程
config::affinity::bind_core_follow_config(0); // 绑核
watcher.watch(&mut |line: String| {
poly.clone().push(line);
LogWatcherAction::None
})
}
发现,成功的选中指定的主线程7号核,子线程6号核,输出如下
core_id num: 8, cores: [CoreId { id: 0 }, CoreId { id: 1 }, CoreId { id: 2 }, CoreId { id: 3 }, CoreId { id: 4 }, CoreId { id: 5 }, CoreId { id: 6 }, CoreId { id: 7 }]
print core_id: CoreId { id: 0 }, bind id: 7
print core_id: CoreId { id: 1 }, bind id: 7
print core_id: CoreId { id: 2 }, bind id: 7
print core_id: CoreId { id: 3 }, bind id: 7
print core_id: CoreId { id: 4 }, bind id: 7
print core_id: CoreId { id: 5 }, bind id: 7
print core_id: CoreId { id: 6 }, bind id: 7
print core_id: CoreId { id: 7 }, bind id: 7
core_id num: 8, cores: [CoreId { id: 0 }, CoreId { id: 1 }, CoreId { id: 2 }, CoreId { id: 3 }, CoreId { id: 4 }, CoreId { id: 5 }, CoreId { id: 6 }, CoreId { id: 7 }]
print core_id: CoreId { id: 0 }, bind id: 6
print core_id: CoreId { id: 1 }, bind id: 6
print core_id: CoreId { id: 2 }, bind id: 6
print core_id: CoreId { id: 3 }, bind id: 6
print core_id: CoreId { id: 4 }, bind id: 6
print core_id: CoreId { id: 5 }, bind id: 6
print core_id: CoreId { id: 6 }, bind id: 6
其中两次选核过程中,也都能够正常打印出所有核。
那么我试一下主线程中先创建A线程,在A线程中绑核(7),然后在主线程中绑核(6),最后创建worker线程,在worker线程中绑核(5),代码如下
fn main() {
let cfg = config::cfg::get_config();
let filename = cfg.las.as_ref().unwrap().access_log.as_ref().unwrap();
let mut watcher = LogWatcher::register(filename.to_string()).unwrap();
thread::Builder::new()
.name("A".into())
.spawn(|| {
config::affinity::bind_core_follow_config(2);
loop {
sleep(Duration::from_secs(1));
}
})
.unwrap();
config::affinity::bind_core_follow_config(0);
sleep(Duration::from_secs(2));
let poly: Poly = Poly::new(); // worker
watcher.watch(&mut |line: String| {
poly.clone().push(line);
LogWatcherAction::None
})
}
结果如下
core CoreId { id: 0 }, bind to: 7
core CoreId { id: 1 }, bind to: 7
core CoreId { id: 2 }, bind to: 7
core CoreId { id: 3 }, bind to: 7
core CoreId { id: 4 }, bind to: 7
core CoreId { id: 5 }, bind to: 7
core CoreId { id: 6 }, bind to: 7
core CoreId { id: 7 }, bind to: 7
>>> core CoreId { id: 7 }, bind to: Thread { id: ThreadId(1), name: Some("main"), .. }
core CoreId { id: 0 }, bind to: 5
core CoreId { id: 1 }, bind to: 5
core CoreId { id: 2 }, bind to: 5
core CoreId { id: 3 }, bind to: 5
core CoreId { id: 4 }, bind to: 5
core CoreId { id: 5 }, bind to: 5
>>> core CoreId { id: 5 }, bind to: Thread { id: ThreadId(2), name: Some("A"), .. }
core CoreId { id: 7 }, bind to: 6
>>> core CoreId { id: 7 }, bind to: Thread { id: ThreadId(3), name: Some("worker"), .. }
可以看到worker进程获取到的核数不正常。worker线程内部略微复杂,里面涉及到数据处理,tokio异步调用的上报等。可是我把worker线程换为简单的替换线程,却没有问题了。代码如下
fn main() {
let cfg = config::cfg::get_config();
let filename = cfg.las.as_ref().unwrap().access_log.as_ref().unwrap();
let mut watcher = LogWatcher::register(filename.to_string()).unwrap();
thread::Builder::new()
.name("A".into())
.spawn(|| {
config::affinity::bind_core_follow_config(2);
loop {
sleep(Duration::from_secs(1));
}
})
.unwrap();
config::affinity::bind_core_follow_config(0);
sleep(Duration::from_secs(2));
// let poly: Poly = Poly::new(); // worker
thread::Builder::new()
.name("B worker".into())
.spawn(|| {
config::affinity::bind_core_follow_config(1);
loop {
sleep(Duration::from_secs(1));
}
})
.unwrap();
watcher.watch(&mut |line: String| {
// poly.clone().push(line);
LogWatcherAction::None
})
}
其中B worker是替换worker的线程,结果如下
core CoreId { id: 0 }, bind to: 7
core CoreId { id: 1 }, bind to: 7
core CoreId { id: 2 }, bind to: 7
core CoreId { id: 3 }, bind to: 7
core CoreId { id: 4 }, bind to: 7
core CoreId { id: 5 }, bind to: 7
core CoreId { id: 6 }, bind to: 7
core CoreId { id: 7 }, bind to: 7
>>> core CoreId { id: 7 }, bind to: Thread { id: ThreadId(1), name: Some("main"), .. }
core CoreId { id: 0 }, bind to: 5
core CoreId { id: 1 }, bind to: 5
core CoreId { id: 2 }, bind to: 5
core CoreId { id: 3 }, bind to: 5
core CoreId { id: 4 }, bind to: 5
core CoreId { id: 5 }, bind to: 5
>>> core CoreId { id: 5 }, bind to: Thread { id: ThreadId(2), name: Some("A"), .. }
core CoreId { id: 0 }, bind to: 6
core CoreId { id: 1 }, bind to: 6
core CoreId { id: 2 }, bind to: 6
core CoreId { id: 3 }, bind to: 6
core CoreId { id: 4 }, bind to: 6
core CoreId { id: 5 }, bind to: 6
core CoreId { id: 6 }, bind to: 6
>>> core CoreId { id: 6 }, bind to: Thread { id: ThreadId(3), name: Some("B worker"), .. }
结论,core_affinity::get_core_ids
获取到的核心信息具有不确定性。查阅作者仓库发现了一个issue与这个问题相关,提issue者同样反馈了这个问题,并质疑get_core_ids获取到的不是所有核,而是对于当前线程的可用核。