介绍

BlackCloud 的博客

Powered by mdbook

Benchmarks

记录一下经常用到的算法/容器的 Benchmark (主要使用 C++)

随用随测, 可能不会特别准确, 工作侧重于在特定负载下选择最好的算法/容器, 所以主要记录不同选择的相对性能

String Hash

  • 4-16 个字符的短字符串
|               ns/op |                op/s |    err% |          ins/op |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|---------------:|--------:|----------:|:----------
|       11,094,427.00 |               90.14 |    0.4% |  111,833,763.00 |  12,383,248.00 |    4.0% |      0.12 | `CRC32`
|        8,181,207.00 |              122.23 |    3.2% |   81,658,267.00 |  12,940,438.00 |    3.8% |      0.09 | `FNV`
|        5,397,684.00 |              185.26 |    0.4% |   56,646,296.00 |   9,432,868.00 |    5.3% |      0.06 | `Murmur2`
|        6,749,671.00 |              148.16 |    1.2% |   67,046,257.00 |   9,432,870.00 |    5.3% |      0.08 | `Murmur3_x86_32`
|       11,833,767.00 |               84.50 |    0.5% |  128,028,092.00 |   5,949,857.00 |    8.5% |      0.13 | `Murmur3_x86_128`
|        7,636,166.00 |              130.96 |    0.8% |   91,747,270.00 |   6,441,582.00 |    7.7% |      0.09 | `Murmur3_x64_128`
  • ~200 个字符的中等长度字符串
|               ns/op |                op/s |    err% |          ins/op |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|---------------:|--------:|----------:|:----------
|      134,931,464.00 |                7.41 |    0.0% |1,235,797,487.00 |  80,973,317.00 |    1.2% |      1.48 | `CRC32`
|      154,305,226.00 |                6.48 |    0.0% |1,250,489,432.00 | 179,927,192.00 |    0.6% |      1.70 | `FNV`
|       41,743,783.00 |               23.96 |    0.0% |  474,381,552.00 |  50,949,403.00 |    1.8% |      0.46 | `Murmur2`
|       44,541,974.00 |               22.45 |    0.1% |  446,744,071.00 |  51,629,173.00 |    1.8% |      0.49 | `Murmur3_x86_32`
|       44,402,780.00 |               22.52 |    0.1% |  461,885,844.00 |  16,677,849.00 |    5.3% |      0.49 | `Murmur3_x86_128`
|       26,934,988.00 |               37.13 |    0.2% |  307,733,470.00 |  16,584,743.00 |    5.3% |      0.30 | `Murmur3_x64_128`

Map

  • 完全随机的 int key, 做 find_or_insert 操作
|               ns/op |                op/s |    err% |          ins/op |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|---------------:|--------:|----------:|:----------
|       35,153,700.00 |               28.45 |    1.8% |  157,273,534.00 |  33,488,895.00 |    1.5% |      0.41 | `unordered_map:random`
|      136,277,935.00 |                7.34 |    1.7% |  193,896,493.00 |  48,073,497.00 |   13.1% |      1.49 | `map:random`
|       58,396,319.00 |               17.12 |    0.8% |  312,029,741.00 |  92,169,201.00 |    4.9% |      0.66 | `btree_map:random`
|       12,382,928.00 |               80.76 |    0.8% |   92,646,655.00 |  12,036,977.00 |    5.7% |      0.14 | `robin_hood_map:random`
|       17,489,723.00 |               57.18 |    1.2% |   90,160,012.00 |  17,922,242.00 |    4.4% |      0.20 | `dense_hash_map:random`
|          810,089.00 |            1,234.43 |    3.2% |    3,600,629.00 |     300,147.00 |    0.0% |      0.01 | `array(test):random`
  • 顺序出现的 int key, 每个 key 重复出现 32 次, 组与组之间略微打乱(比如 0 0 0 1 0 1 1 2 1 2 2 2 ... 这样的序列), 做 find_or_insert 操作
|               ns/op |                op/s |    err% |          ins/op |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|---------------:|--------:|----------:|:----------
|       19,428,866.00 |               51.47 |    1.2% |  137,639,829.00 |  26,979,609.00 |    1.2% |      0.22 | `unordered_map:sequential`
|      305,547,283.00 |                3.27 |    0.5% |  494,128,064.00 | 130,782,008.00 |   17.3% |      3.34 | `map:sequential`
|      183,810,512.00 |                5.44 |    0.4% |  973,906,327.00 | 302,459,238.00 |    4.8% |      2.03 | `btree_map:sequential`
|       34,349,617.00 |               29.11 |    0.3% |  183,005,851.00 |  24,127,612.00 |   12.6% |      0.38 | `robin_hood_map:sequential`
|       26,675,597.00 |               37.49 |    1.2% |  185,300,938.00 |  46,039,048.00 |    5.1% |      0.30 | `dense_hash_map:sequential`
|        6,950,046.00 |              143.88 |    0.8% |   38,400,593.00 |   3,200,143.00 |    0.0% |      0.08 | `array(test):sequential`

Idioms

1. CRTP

我想实现如下伪代码所示的编程模式, 其中 Host 代表 C/S 模型中的任何一个端点, ServerClient 都需要处理收到的包, 但对于每个类型的包, 它们有不同的处理方式. 收包的逻辑是相同的, 因此我希望复用 handle_packet. 最简单的方法就是用虚函数

class Host {
    void handle_packet(...) {
        // do receive packet
        switch (packet_type) {
        case (a):
            handle_packet_a();
            break;
        case (b):
            handle_packet_b();
            break;
        };
    };
    virtual void handle_packet_a();
    virtual void handle_packet_b();
};

class Server: public Host {
    virtual void handle_packet_a() override;
    virtual void handle_packet_b() override;
};

class Client: public Host {
    virtual void handle_packet_a() override;
    virtual void handle_packet_b() override;
};

但这样设计只是为了复用 handle_packet 的逻辑, 而没有动态绑定的需求, 因此引入虚函数增加了没有必要的开销

这种编译期多态可以通过 CRTP 实现

template<typename Derived>
class Host {
    void handle_packet() {
        switch (packet_type) {
        case (a):
            static_cast<Derived&>(*this).handle_packet_a();
            break;
        case (b):
            static_cast<Derived&>(*this).handle_packet_b();
            break;
        };
    }
};

class Server: public Host<Server> {
    void handle_packet_a();
    void handle_packet_b();
};
// ...

这样可以完全去除虚函数

2. SFINAE

Substitution Failure Is Not An Error

起因是想在用 CRTP 时实现编译时检测子类是否具有某个方法, 仅当有方法时才调用的需求, 例如:

template<typename D>
class Animal {
    void live() {
        while (true) {
            static_cast<D&>(*this).eat();
            static_cast<D&>(*this).sleep();
            static_cast<D&>(*this).bark(); // compile error! Cat will never bark
            // Want: if constexpr (D has member `bark`) then call `bark`
        }
    }
};

class Cat: public Animal<Cat> {
    void eat();
    void sleep();
};

class Dog: public Animal<Dog> {
    void eat();
    void sleep();
    void bark();
};

这种在编译期模板实例化时确定模板参数是否有某种性质的行为被称为内省(introspection)

包含模板的重载函数的候选集中的某些(或者全部)候选函数来自 模板实参替换模板形参 的模板实例化结果, 在这个过程中, 某些模板的实参替换可能会失败, 这种替换失败(Substitution Failure)并不会立即被当作编译错误(Error)抛出, 这个替换失败的模板会被从候选集中删除, 只要到最后存在成功的替换, 即重载函数候选集不为空, 则这个重载函数的解析就是成功的, 编译也能通过

见来自 替换失败并非错误 的例子:

struct Test {
  typedef int foo;
};

template <typename T>
void f(typename T::foo) {}  // Definition #1

template <typename T>
void f(T) {}  // Definition #2

int main() {
  f<Test>(10);  // Call #1.
  f<int>(10);   // Call #2. 并无编译错误(即使没有 int::foo)
                // thanks to SFINAE.
}

在编译时, f<Test>(10) 会针对 f 的两个定义做两次 Substitution

  1. 第一次替换得到的函数定义是 void f(typename Test::foo) {}
  2. 第二次替换得到的函数定义是 void f(Test) {} 因此这个调用拥有两个可能的候选, 而根据实参 10 的类型可以推导得到只有 1. 符合要求, 因此最终会调用 1., 编译通过

f<int>(10) 也会针对 f 的两个定义做两次 Substitution

  1. 得到 void f(typename int::foo) {}, 但 int::foo 并不存在, 因此这个替换失败了, 这个函数并不会进入候选集
  2. 得到 void f(int) {} 因此这个调用拥有一个可能的候选, 而实参 10 的类型可以匹配这个唯一的候选, 因此最终会调用 2., 编译通过

上面的例子里, SFINAE 恰好干了在开头时我想干的事: 在编译时判断一个成员是否存在于 struct/class 中.

经过修改可以得到以下真正实现了这个需求的代码, 检查类型 T 上是否有拥有 bark 成员函数:

template<typename T>
struct has_member_bark {
    private:
        template<typename U> static auto check(int) 
            -> decltype(std::declval<U>().bark(), std::true_type());
        template<typename U> static std::false_type check(...);
    public:
        enum {value = std::is_same<decltype(check<T>(0)), std::true_type>::value};
};

// 接上面 Animal 的例子...
if constexpr (has_member_bark<D>::value) {
    static_cast<D&>(*this).bark();
}

上述例子工作的原理是

  1. 编译时要对 has_member_bark<T>::value 求值, 其值等于 std::is_same<decltype(check<T>(0)), std::true_type>::value

  2. 需要推导 decltype(check<T>(0))

  3. check<T>(0) 有两个可选的模板

    • template<typename U> static auto check(int) -> decltype(std::declval<U>().bark(), std::true_type())
    • template<typename U> static std::false_type check(...)

    其中后者由于指定了 ... 作为参数, 因此拥有最低的匹配优先级, 所以编译器会优先尝试第一个定义

    第一个定义的返回值需要被推导, 其类型为

    decltype(std::declval<U>().bark(), std::true_type())
    

    decltype 内是一个 comma expr, 其值等于最后一个表达式的值, 但是求值是从前到后进行的, 因此必须先推导 std::declval<U>().bark() 的类型, 这时

    • 如果 U::bark 不存在, 则这个替换就会失败, 因此 check 的第一个模板就会被删除, 只留下第二个, 则 decltype(check<T>(0))false_type
    • 如果 U::bark 存在, 则这个替换成功, check 的第一个模板成为最终的选择, decltype(check<T>(0))true_type
  4. 无论 T::bark 是否存在, 由于 SFINAE, 最终总有一个 check 被匹配, 如果 T::bark 存在, 则 value 最终为 true_type, 否则是 false_type

一个完整的可编译的例子:

#include <iostream>

template<typename T>
struct has_member_bark {
    private:
        template<typename U> static auto check(int) 
            -> decltype(std::declval<U>().bark(), std::true_type());
        template<typename U> static std::false_type check(...);
    public:
        enum {value = std::is_same<decltype(check<T>(0)), std::true_type>::value};
};

template<typename D>
class Animal {
public:
    void live() {
        // while (true) {
            static_cast<D&>(*this).eat();
            static_cast<D&>(*this).sleep();
            if constexpr (has_member_bark<D>::value) {
                static_cast<D&>(*this).bark();   
            }
        // }
    }
};

class Cat: public Animal<Cat> {
public:
    void eat() {
        std::cout << "Cat eat" << std::endl;
    }
    void sleep() {
        std::cout << "Cat sleep" << std::endl;
    }
};

class Dog: public Animal<Dog> {
public:
    void eat() {
        std::cout << "Dog eat" << std::endl;
    }
    void sleep() {
        std::cout << "Dog sleep" << std::endl;
    }
    void bark() {
        std::cout << "Woof!" << std::endl;
    }
};

int main() {
    Cat().live();
    Dog().live();
    return 0;
}

输出

Cat eat
Cat sleep
Dog eat
Dog sleep
Woof!

3. PIMPL

Pointer to IMPLementation

写了一个库, 起初供用户 include 的头文件里有这样的声明:

class Server {
    public:
        Server();
        void start();
    private:
        void receive_packets();
        void handle_packet();
        void handle_packet_data();
        void handle_packet_ping();
        void handle_packet_pong();
    private:
        int socket;
        // more members ...
};

但首先, 暴露给用户的接口只有 start, 用户在看头文件时只需要看到他能使用的接口即可, 看到一堆其他的东西会干扰阅读, 其次暴露太多实现细节也不好

可以用 pimpl 来实现 implementation 的隐藏, 原理比较简单, 只贴代码了:

// server.h
class Server {
    public:
        Server();
        void start();
    private:
        class ServerImpl;
        std::unique_ptr<ServerImpl> impl;
};

// server.cpp
Server::Server(): impl(std::make_unique<ServerImpl>()) {}
void Server::start() {
    while (true) {
        impl->receive_packets();
    }
}

class Server::ServerImpl {
    public:
        void receive_packets() {
            // ...
        }
    private:
        void handle_packet();
        void handle_packet_data();
        void handle_packet_ping();
        void handle_packet_pong();
    private:
        int socket;
        // more members ...
}

这样所有的实现细节都被隐藏到 source file 里了

lambda, std::function, etc

1. 问题

之前写代码遇到了一些传递回调函数的需求, 例如:

receive_packet_and_handle([](const char* buf, int len){
    // do sth. with buf
});

这样 receive_packet_and_handle 有两种写法:

  1. template<typename F>
    void receive_packet_and_handle(F&& handler) {
        // ...
    }
    
  2. using handler_t = std::function<void(const char*, int)>;
    void receive_packet_and_handle(const handler_t& handler) {
        // ...
    }
    

想知道两种传参有没有性能上的不同

2. 结论

先说结论, template 风格性能通常比 std::function 风格好, 前者能 inline lambda, 后者通常不能 inline, 可能还需要构造 std::function 对象

参考 can-be-stdfunction-inlined-or-should-i-use-different-approach 的回答

std::function is not a zero-runtime-cost abstraction. It is a type-erased wrapper that has a virtual-call like cost when invoking operator() and could also potentially heap-allocate (which could mean a cache-miss per call).

The compiler will most likely not be able to inline it.

If you want to store your function objects in such a way that does not introduce additional overhead and that allows the compiler to inline it, you should use a template parameters. This is not always possible, but might fit your use case.

不过之前想用 std::function 的另一个原因是它能约束传入的 lambda 的参数及返回值, 用模板的话当传入的函数签名不符合预期时会有比较晦涩的报错, 而且函数使用者也很难直接从函数声明看出来到底要传入什么 lambda, 返回值如何, 可读性很差

对于这个问题, 查到的解决办法是用 std::invocable, 参考 how-can-i-restrict-lambda-signatures-in-c17-template-arguments

3. 汇编

在 Compiler Explorer 上用 x86-64 gcc 12.2 测试如下代码

template<typename F>
void with_tempalte(F&& f) {
    f(10);
}

void with_function(std::function<void(int x)>&& f) {
    f(10);
}

static volatile int a;
void test() {
    // #1
    with_tempalte([](int x){
        a = x;
    });

    // #2
    with_function([](int x){
        a = x;
    });
}

O0

1. 调用 with_template

lea     rax, [rbp-65]
mov     rdi, rax
call    void with_tempalte<test()::{lambda(int)#1}>(test()::{lambda(int)#1}&&)

2. 调用 with_function

lea     rdx, [rbp-17]
lea     rax, [rbp-64]
mov     rsi, rdx
mov     rdi, rax
call    std::function<void (int)>::function<test()::{lambda(int)#2}, void>(test()::{lambda(int)#2}&&)
lea     rax, [rbp-64]
mov     rdi, rax
call    with_function(std::function<void (int)>&&)
lea     rax, [rbp-64]
mov     rdi, rax
call    std::function<void (int)>::~function() [complete object destructor]
jmp     .L19
mov     rbx, rax
lea     rax, [rbp-64]
mov     rdi, rax
call    std::function<void (int)>::~function() [complete object destructor]
mov     rax, rbx
mov     rdi, rax
call    _Unwind_Resume

std::function 时会构造 std::function 对象, 后者显然更低效

O1

1.

lambda 被直接内联了, 汇编只有

mov     DWORD PTR a[rip], 10

2.

mov     rdi, rsp
call    with_function(std::function<void (int)>&&)

with_function(std::function<void (int)>&&):
        sub     rsp, 24
        mov     DWORD PTR [rsp+12], 10
        cmp     QWORD PTR [rdi+16], 0
        je      .L9
        lea     rsi, [rsp+12]
        call    [QWORD PTR [rdi+24]]
        add     rsp, 24
        ret
.L9:
        call    std::__throw_bad_function_call()

仍然存在对 std::function 对象的调用, 没有内联

O3

        mov     DWORD PTR a[rip], eax
        ret
with_function(std::function<void (int)>&&):
        sub     rsp, 24
        cmp     QWORD PTR [rdi+16], 0
        mov     DWORD PTR [rsp+12], 10
        je      .L10
        lea     rsi, [rsp+12]
        call    [QWORD PTR [rdi+24]]
        add     rsp, 24
        ret
.L10:
        call    std::__throw_bad_function_call()
test():
        mov     DWORD PTR a[rip], 10 # with_template
        mov     DWORD PTR a[rip], 10 # with_function
        ret

O3 情况下二者都被内联了, 但此时 with_function 仍然出现在了汇编结果里

malloc c++ class 而不调用构造函数引发的 segfault

背景

原本有一个纯 C 的库,其某结构体内保存了一个函数指针作为 callback

struct Foo {
    int (*output)(...);
};

为了在 C++ 中方便地使用 lambda 等,我自作主张地将其改成了

struct Foo {
    std::function<int(...)> output;
};

起初工作得很好,std::function 在替代函数指针方面非常方便。直到将代码放到另一套环境中时发现会随机 segfault,gdb 调试定位到了该回调相关的部分。一定是遇到了 Undefined Behavior 了

排查

出现问题的代码片段位置不尽相同,但是总是出现在 给该结构体的 output 成员赋值 上,如

// when creating a empty Foo instance, give it's member a default value
foo->output = std::function<int(...)>();

segfault 的 backtrace 最终总是

#0  0x0000000000404ce4 in std::_Function_base::~_Function_base (this=0x7fffffffc3c0, __in_chrg=<optimized out>)
#1  0x000000000040ca7e in std::function<int (...)>::~function()
#2  0x00000000004180df in std::function<int(...)>::operator=<::<lambda(...)> >::<lambda(...)> >(struct {...} &&) 

即调用 std::functionoperator= 赋值时,调用了某个 std::function 的析构函数,然后析构函数触发了 segfault。根据赋值的位置,应当是将新的 output 赋值到成员变量时,原本成员变量的 output 需要析构

然后想到因为是纯 C 的库,申请 Foo 的位置均使用 mallocmalloc 默认不会构造结构体及其 member 的构造函数,因此得到的 foo->output 的位置上也是未初始化的、随意一段内存,却被当成了一个合法的 std::function。在这段随意的内存上试图调用 std::function 的函数则成为了 UB,导致了 segfault

因此解决方式也很简单,在 malloc 之后手动 placement new 一下结构体(或者是单独初始化 output)即可

Foo* foo = (Foo*)malloc(sizeof(Foo));
new (&(foo->output)) std::function<int(...)>();

部署 mdbook 到 github pages

本网站由 mdbook 生成,通过 github actions 持续集成到 github pages,以下为部署方式

1. 为项目创建仓库

BlackCloud37/mdbook-blog,其 master 分支为 mdbook 项目

2. 添加 Github Action

参考 GitHub Pages Deploy,创建 .github/workflows/deploy.yml,内容为

name: Deploy
on:
  push:
    branches:
      - master

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
      with:
        fetch-depth: 0
    - name: Install mdbook
      run: |
        mkdir mdbook
        curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.14/mdbook-v0.4.14-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook
        echo `pwd`/mdbook >> $GITHUB_PATH
    - name: Deploy GitHub Pages
      run: |
        # This assumes your book is in the root of your repository.
        # Just add a `cd` here if you need to change to another directory.
        mdbook build
        git worktree add gh-pages
        git config user.name "Deploy from CI"
        git config user.email ""
        cd gh-pages
        # Delete the ref to avoid keeping history.
        git update-ref -d refs/heads/gh-pages
        rm -rf *
        mv ../book/* .
        git add .
        git commit -m "Deploy $GITHUB_SHA to gh-pages"
        git push --force --set-upstream origin gh-pages

之后所有到 master 的 push 都会 build 当前的 mdbook,并将产物 push 到本仓库的 gh-pages 分支

3. 访问网站

gh-pages 分支下的网页会被部署到 <Username>.github.io/<Reponame> 域名下,如本项目即为 blackcloud37.github.io/mdbook-blog,直接访问即可

4. Trouble shooting

第一次推送触发 github action 后,gh-pages 分支存在并且已经包含产物,但是访问 blackcloud37.github.io/mdbook-blog 提示 404,参考 https://stackoverflow.com/questions/11577147/how-to-fix-http-404-on-github-pages,推送一个空 commit 到 gh-pages 分支即可:

# 在 gh-pages 分支
git commit --allow-empty -m "Trigger rebuild"
git push

另外,仓库可见性必须为 Public

Arch 休眠到交换文件

参考 Arch WikiArch简明指南 配置系统休眠到 swap file(ext4),配置完毕后无法正常休眠,问题如下

1. KDE 开始菜单不展示休眠选项

尝试手动休眠 systemctl hibernate,提示 "Not enough swap space for hibernation"

根据 Arch BBS,通过 systemctl edit systemd-logind.service 并在其中添加

# 注意添加位置,必须在文件中注明的两段注释之间,否则不会生效
[Service]       
Environment=SYSTEMD_BYPASS_HIBERNATION_MEMORY_CHECK=1

后重启 logind 服务 systemctl restart systemd-logind 即可

2. 休眠后立刻回到登录页面

休眠后查看日志 journalctl -n 1000,在其中查找 hibernate 相关记录,发现报了

Failed to find location to hibernate to: Function not implemented

怀疑是 hibernate 目标交换文件配置有误,检查后发现在获取交换文件 resume_offset 时,用 sudo filefrag -v /swapfile 命令查看的偏移如下:

Filesystem type is: ef53
File size of /swapfile is 34359738368 (8388608 blocks of 4096 bytes)
 ext:     logical_offset:        physical_offset: length:   expected: flags:
   0:        0..    6143:    4114432..   4120575:   6144:            
   1:     6144..   38911:    3997696..   4030463:  32768:    4120576:
   2:    38912..   71679:    3506176..   3538943:  32768:    4030464:
   3:    71680..  104447:    8224768..   8257535:  32768:    3538944:
   ...

physical_offset列的第一个值应当是 4114432,而我配置成了 4120575,修改后重新生成 grub.cfg 即可

Wechat

使用打包好的 Deepin Wine Wechat Arch,仓库中已经给出了详细的安装方法、字体更换等

Sway

从 i3 迁移到 sway 后, Deepin Wine Wechat 的微信窗口黑屏, 暂时没找到解决办法, 改用 wine-for-wechat 后解决

安装流程

  1. 安装 wine-for-wechat, wine-wechat 包, 均在 archlinuxcn 源上
  2. 微信官网下载 .exe 安装包
  3. 命令行 wechat -i /path/to/wechat_setup.exe 安装微信
  4. 安装完成后就能使用, 如果字体有问题, 参考 Ubuntu20.04 Wine 6.0 微信中文显示方块/方框, 本质上为如下几步
  • winetricks 安装所有字体和所需 dll
  • 改注册表

不过仍然没解决 emoji 为方块的问题

Trouble shotting

窗口阴影

换到 i3wm 后,混成器使用了 picom,此时使用 Deepin Wine Wechat 会发现整个窗口被灰色遮罩,且有弧形的黑色阴影,关闭 picom 后上述情况消失,因此判断是 picom 的问题

查阅 arch wiki 上 picom 条目发现,picom 可以针对窗口禁用半透明、阴影等特性,其规则可以细化到匹配窗口名称

首先通过 xprop 查询微信的窗口名:

# xprop
WM_NAME(STRING) = "WeChat"
...
WM_CLASS(STRING) = "wechat.exe", "Wine"

然后在 picom.conf 里添加如下规则:

~/.config/picom/picom.conf
# Specify a list of conditions of windows that should have no shadow.
#
# examples:
#   shadow-exclude = "n:e:Notification";
#
# shadow-exclude = []
shadow-exclude = [
  # ...
  "name = 'WeChat'",
  "class_g = 'wechat.exe'",
  "class_g = 'Wine'"
];

重启 picom 即可

字体发虚

打开 wine 设置

# /opt/apps/com.qq.weixin.deepin/files/run.sh winecfg

graphics 选项卡里调高DPI即可

部分 Emoji 为方块

尚未解决

i3wm 切换到 sway

复制配置

mkdir -p ~/.config/sway
cp ~/.config/i3/config ~/.config/sway/

重映射 CapsLock 到 Ctrl, 修改键盘重复速率

原来用的是 setxkbmap, 但这是针对 xorg 的

映射方法为在 sway config 中加入

input "type:keyboard" {
    xkb_options caps:ctrl_modifier
    repeat_delay 150
    repeat_rate 80
}

Deepin Wine Wechat 黑屏

Wechat

其他

  • 使用 wofi 替换 rofi
  • 使用 waybar 替换 polybar
  • 使用 swaybg 替换 feh

vscode 无法记住登录

装了 visual-studio-code-bin, 发现每次启动一个新的实例时都要求登录以同步设置, 即无法记住其他实例的登录

最后参考 Settings Sync in Visual Studio Code 解决了

Linux 上的 VSCode 依赖 gnome-keyring 来保存认证信息, 所以需要正确安装并配置 gnome-keyring, 我之前尝试过装 gnome-keyring 但最后还是没解决, 可能是因为当时装了另一个 keyring 包, 二者冲突了

具体步骤

  1. ~/.xinitrc 里添加如下行

    # see https://unix.stackexchange.com/a/295652/332452
    source /etc/X11/xinit/xinitrc.d/50-systemd-user.sh
    
    # see https://wiki.archlinux.org/title/GNOME/Keyring#xinitrc
    eval $(/usr/bin/gnome-keyring-daemon --start)
    export SSH_AUTH_SOCK
    
    # see https://github.com/NixOS/nixpkgs/issues/14966#issuecomment-520083836
    mkdir -p "$HOME"/.local/share/keyrings
    

    由于我在用 swaywm, 所以我把上面这些行加到了 sway 的 config 里, 最后也能 work

  2. 重新登录以加载 1. 的配置

  3. 安装 gnome-keyring etc., sudo pacman -S gnome-keyring libsecret libgnome-keyring

  4. 使用任何 gnome-keyring 的管理手段 (比如 seahorse), 解锁默认的 keyring 或者创建一个新的没上锁的 keyring. 在我的尝试里, 这一步如果不做, 启动 vscode 并登录账号时会提示新建 keyring, 新建时不要设置密码即可

  5. 启动 vscode

自动认证 Tsinghua WiFi

在 Mac 连接到 Tsinghua/Tsinghua-5G 等需要认证的无线网时自动认证而无需经 Web 认证

0. 准备

下载

1. auth-thu

用于从命令行认证校园网

  • 给予可执行权限: chmod +x auth-thu.macos.arm64
  • 放到 $PATH 下并重命名为 auth-thu: cp auth-thu.macos.arm64 /usr/local/bin/auth-thu
  • 在 HOME 目录下创建其配置: touch ~/.auth-thu
  • 在配置内写入校园网账号密码 (这里只能明文): vim ~/.auth-thu
    {
        "username": "username",
        "password": "password"
    }
    
  • 尝试认证看正不正常
    > auth-thu
    2022-11-23 17:08:41 INFO auth-thu main.go:308 Currently online!
    

2. WifiWatch.app

直接运行 .app 即可,这个程序会在后台,当连接/断开 WiFi 时执行特定脚本

  • 连接时执行 ~/.wifiConnected
  • 断开时执行 ~/.wifiDisconnected (本示例用不到)

可以在系统设置里把这个 .app 加到开机启动项

3. .wifiConnected

连接 WiFi 时执行的脚本

写入如下内容 vim ~/.wifiConnected

#!/bin/bash
# arg1: SSID of network
# arg2: SSID of old network, if any

log=/tmp/auth-thu
if [[ "$1" =~ ^(Tsinghua|Tsinghua-5G)$ ]]; then
	for i in 4 3 2 1; do
		sleep $i
		connected=$(/usr/local/bin/auth-thu 2>&1 | tee -a $log | grep -E "online|Successfully" && echo 0 || echo 1)
		if [[ $connected == 0 ]]; then
			sleep $i
			/usr/local/bin/auth-thu 2>&1 | tee -a $log
		else
			break
		fi
	done
fi

然后给予可执行权限 chmod g+x ~/.wifiConnected

4. 尝试

  • 退出认证
  • 断开 WiFi 然后连接 Tsinghua/Tsinghua-5G
  • 检查 /tmp/auth-thu 里是否有 log
  • 检查 WiFi 是否正常认证

5. Trouble Shooting

WiFi 名目前是字符串匹配且只有 Tsinghua/Tsinghua-5G,如果需要其他的,加在 .wifiConnected

6. Reference

  • https://apple.stackexchange.com/questions/139267/run-program-if-connected-to-specific-wifi
  • https://github.com/p2/WifiWatch
  • https://github.com/z4yx/GoAuthing

5600x 超频

1. 平台

  • R5 5600x
  • 乔思伯 HX6200D 风冷
  • MSI B550I Gaming Edge Max Wifi
  • 铂胜白条 3000MHZ 8G * 2
  • RX584
  • 300w电源

装在蜂鸟 i100pro 这个 itx 机箱里

2. 参数

内存小超到了 3800 16-19-19-36 1.39V

这个电脑只用来打游戏,由于机箱没有风扇位,而且是下压式风冷,开 PBO2 之后 Aida64 烤 FPU 温度巨高,打游戏掉帧,索性锁频降压使用

全核锁 4.6GHZ,电压在主板里给 1.1375V,防掉压之类的全 Auto,室温 20 度

  • 开机箱盖,单烤 FPU 能稳定 10 分钟,温度稳定在 79 度
  • 关机箱盖,单烤 FPU 2 分钟后蓝屏,温度到 82 度

全核锁 4.6GHZ,电压在主板里给 1.1625V

  • 开机箱盖没测
  • 关机箱盖,单烤 FPU 8 分钟后黑屏重启,温度到 86 度

之后还是想关机箱盖用,考虑到打游戏负载也不会多高,暂时就用 4.6GHZ/1.1625V 的参数跑,打游戏的时候崩溃再说, 这个参数下:

  • CPU-Z 单核 633.9,多核 5005.6
  • 待机 41 度
  • 守望先锋:归来 2K 极高画质下 55 度,能稳定在 60FPS 以上

UPDATE:

  • 关闭超线程,参数修改为 4.575GHZ/1.1V,关机箱盖 Aida64 单烤 FPU 稳定 69 度,超过 15 分钟不崩溃,感觉是一个更合适的设置
  • 单核跑分 631,关超线程多核就不看了

Auth THU

THU 宿舍校园网需要网页认证, 不过有同学开发了 GoAuthing 这个认证程序, 在常规的 Linux 下可以直接开 crontab/service 定时认证, 但 QNAP 的 crontab 似乎有点问题, 我的解决方案是开了一个 Docker 容器跑认证程序

Dockerfile如下

FROM alpine:latest

COPY ./auth-thu /usr/local/bin/auth-thu
COPY ./.auth-thu /root/.auth-thu

# 这里输出 log 是为了之后验证 auth-thu 是否正确执行以 debug, 如果不需要 debug 则使用:
# RUN echo '*/1 * * * * /usr/local/bin/auth-thu auth' > /etc/crontabs/root
RUN echo '*/1 * * * * /usr/local/bin/auth-thu auth >> /var/log/auth-thu.log 2>&1' > /etc/crontabs/root

CMD crond -l 2 -f

用了 alpine 这个非常小的 linux 镜像, 使用方式为

  1. 创建 Dockerfile 并添加上面的内容
  2. 在 Dockerfile 同目录下下载 GoAuthing 的可执行文件并改名为 auth-thu, 同时赋以可执行权限
  3. 在 Dockerfile 同目录下创建 .auth-thu 文件, 填入 GoAuthing 的相关配置, 如用户名密码
  4. 在该目录下用 Dockerfile 创建镜像, 例如 docker create -t auththu:latest .
  5. 用创建好的镜像创建容器, 并设置其为自动重启(即随 docker 启动) docker run -d --name auththu --net=host --restart=always

QNAP Crontab 使用

  • crontab 配置文件在 /etc/config/crontab
  • 应用修改 # crontab /etc/config/crontab
  • 重启服务 # /etc/init.d/crond.sh restart

注意定时任务所用的脚本不能放在根目录下,因为 QNAP 每次重启都会重置根目录,建议放在 /share/homes/<username>/

QNAP Hlink Docker 使用

环境

  • QNAP 453B mini,事先安装好 Container Station,配置 SSH 登录
  • 我的 qBittorrent 下载文件均在 /share/Download 目录下,希望将里面的文件硬链到 /share/Media
    • 硬链目录不能跨盘,DownloadMedia 两个共享文件夹均在同一块硬盘同一个存储池上

部署

直接用 Container Station 部署时,hlink 会报 必须指定配置文件 的错,原因不明,因此这里用命令行部署

ssh 到 nas:

docker run -d --name hlink \
    -e HLINK_HOME=/share/Container/Docker/Hlink \
    -p 9090:9090 \
    -v /share:/share \
    likun7981/hlink:latest

将 nas 的 /share 挂载到 container 的 /share 下,同时指定 /share/Container/Docker/Hlink 为 hlink 的家目录。nas 的 9090 端口映射为容器的 9090 端口,即 hlink 的 WebUI

然后访问 :9090,创建配置文件和定时计划即可

Trouble shooting

目前(2022-10-17)hlink 的 WebUI 不支持账号功能,即一旦暴露到公网,任何人都可以访问该 WebUI,而我的 nas 直连校园网,自带公网 IP,因此需要考虑安全问题

暂时的解决办法是,考虑到一旦配置好 hlink 服务,便不太需要访问其 WebUI,因此可以在完成配置后用 iptable ban 掉 WebUI 的端口

#!/bin/bash

number=`iptables -t nat --line-numbers --numeric --list | grep dpt:9090 | awk '{print $1}'`
if [ -n "$number" ]; then
echo $number
iptables -t nat -D DOCKER $number
fi

qnap 的 iptable 规则每次重启后都会失效,因此可以将上述脚本加入 crontab

qBittorrent

安装

  1. App Center 添加软件源 https://www.qnapclub.eu/repo.xml
  2. 搜索 qBittorrent 安装

配置

账号

默认端口 6363,默认账号 admin,默认密码 adminadmin

进 设置-WebUI 修改端口、账号密码、语言等为想要的

HTTPS

在 设置-WebUI 选择使用 HTTPS 而不是 HTTP,证书和密钥设为域名证书的 .pem.key 文件的绝对路径

下载排队

在 设置-BitTorrent 启用或关闭 Torrent 排队和做种限制

校园网 IPv6 (北邮人 PT)

  • 设置-高级-qBittorrent 相关
    • 网络接口 改为有校园网 IPv6 的接口
    • 绑定到可选的 IP 地址 改为 所有 IPv6 地址
    • 如果 Tracker 显示 SSL 证书错误,则取消勾选 验证 HTTPS tracker 证书

LEDE 旁路由

背景

PS5 想翻墙, 想在 nas 里装软路由然后装个 clash 给 PS5 当旁路由

步骤

安装 LEDE

  1. 参考 威联通Docker教程 篇十:威联通NAS安装LEDE软路由,保姆级教程,手把手教您虚拟机安装openwrt旁路由 安装 lede, 这一步没什么坑
  2. 装好后, 由于 453bmini 有两块网卡, 第一块网卡我直接连了校园网做主网关, 第二块网卡连路由器给局域网用, 因此需要创建一个连接第二块网卡和 lede 虚拟机接口的虚拟交换机, 然后在 virtualization station 里配置虚拟机的网络为该虚拟交换机

安装 clash

  1. 使用的 clash 版本为 Koolshare-Clash-hack, 下载 release 的 tar 包用 lede 自带的离线安装
  2. 直接离线安装会提示非法关键字, 无法安装, 需要用这里提到的 hack 禁用关键字扫描规则, 核心是 ssh 上 lede 然后跑这个命令
    sed -i 's/\tdetect_package/\t# detect_package/g' /koolshare/scripts/ks_tar_install.sh
    

使用 clash

  1. 配置订阅链接, 启用 clash
  2. 需要在 clash 里开启需要走代理的设备的 IP
  3. 在 PS5 里修改网关为 lede 的 IP

吐司

2023/01/26 制作

参考

材料等

450g 的吐司模用料

  1. 250g 高筋粉
  2. 75g 全蛋液
  3. 90g 牛奶
  4. 30g 白砂糖
  5. 25g 黄油
  6. 5g 干酵母
  7. 3g 盐

步骤

  1. 干酵母、黄油、盐除外,所有材料混合均匀,吸收至不见干粉,成团,密闭冷藏 30min - 12h,越长越好

    这一步可以让面团水合,并降低面温,方便揉出组织。因此时间久一点比较好

    制作时,实际冷藏了 4h 左右。由于用的砂糖是粗砂糖、蛋液没有打得很匀、天气较冷等原因,面团混合得不够好,导致水合不充分。之后制作可以混合得更充分一些再静置

  2. 准备好酵母、黄油和盐,酵母可以略加水(5g)搅拌成膏状。为了避免提前发酵,应控制酵母温度。之后揉面时面团也应控制在较低温度(28摄氏度-)
  3. 取出面团,揉面
  4. 揉面
    • 揉面大抵分成两步,先是混合所有材料,然后是充分揉搓直到出现手套膜

      混合材料阶段,手法是

      • 切块:将面团切块以增大表面积,然后均匀地加上某一材料,然后将所有块重新搓成一块
      • 揉搓:用一手固定面团位置,另一手将面团外推,类似在搓衣板上搓毛巾,将整个面团搓成扁扁的一坨后重新折叠收集成一坨,重复。直到添加的材料消失

      重复上述步骤直到所有材料混合均匀,材料的加入顺序为:盐、酵母、黄油

      然后是揉搓阶段,由于这一步耗时较久且面团长时间接触手,应当注意控制面温

      • 面团初始为一坨,同样是一手固定,另一手将其用力往外搓,搓成一滩后重新折叠收集成一坨。重复搓
      • 可以不时拾起面团用力向下摔到桌面,摔成长条后重新折叠成一坨

      重复上述两个步骤,总之就是不断让面团从一团变成一滩或是一条,然后复原

      等到面团不再粘手或粘桌面时,尝试在面团上撕下一小块,撕的手感类似于撕橡胶,有点筋道的感觉,则可以尝试拉膜,如果能拉出较薄的膜就差不多了

  5. 发酵
    • 整形:将面团均匀分割成 3 至 4 份,每份擀成宽度均匀的长薄片,然后从一端开始卷成一卷。然后将这个卷按长的方向(即擀面杖垂直于面团卷这一圆柱体的高)重新擀成薄片,再卷成卷。卷好后直接并排均匀放置到吐司模中,卷的长边与吐司模的短边平行
    • 发酵:密封吐司模,在合适的环境中发酵至吐司模 7-8 分满

      我采用单次发酵法,时间是冬天,发酵较慢。将吐司模密封放置在 10-20 摄氏度左右的烤箱内,花了 3.5h 左右发酵到预期高度。有温湿度计的可以看一些发酵指南精细控制。不再赘述。期间注意多检查面团状态

  6. 烤制
    • 密封吐司模
    • 柏翠烤箱,上下管 160 摄氏度,预热 10min,烤 30min
  7. 烤完后立刻脱模,侧向静置冷却

贝果

2023/01/28 制作

参考

材料和步骤均参考该帖子,这里只记录注意点

注意

  1. 在原配方的基础上,面粉的吸水性各有不同,比如这次做的时候按原方的水量揉出来的面团非常干,导致后续的整形等都不容易,之后可以适当增减水量
  2. 面团需要揉到扩展阶段,但不需要出手套膜,等面团变得光滑且有韧性应该就差不多了
  3. 整形的时候面团需要适当擀得大些均匀些,折叠之后要把气泡排空,卷起时注意接缝的密闭,否则膨胀之后非常容易开裂
  4. 圈可以适当做的大些,面团的膨胀空间很大,圈太小最后中间的洞就没了
  5. 做的时候只关注上侧有没有焦,结果底被烤焦了。需要注意这种看不见的地方