iseki 的笔记本

没什么,只是个备忘录

这里是指,不要继续在人类可读的文本类传输格式(例如 JSON)中继续使用「时间戳」,而那些二进制格式,体积敏感的不在讨论范围内。

很长一段时间以来,我一直认为当大家提起「时间戳」这个名字时,大家都会无差别的理解为 UNIX time,即从 1970-01-01T00:00:00Z 到此时此刻的秒数,无视闰秒,每天都是准确的 86400 秒。
这个定义是标准化的,在 POSIX 标准和 Open Group Base Specification 中都有所涉及。

但近来,我发现真的有部分开发者对时间戳的理解存在差异,原因很简单,很多语言中提供的类时间戳表示都以毫秒为单位。比如在 Java 中,System.currentTimeMillis() 方法返回的是一个自 1970-01-01T00:00:00Z 至此时此刻的毫秒数;在 JavaScript 中,原生提供的 Date 类型,不管是构造器还是数值表示(指进行数值运算时进行的隐式转换)都以毫秒处理。
这导致每当我提起时间戳时,我不得不明确强调是 UNIX time,有时还要额外提醒,以秒为单位(总有些人不能立刻理解我强调 UNIX time 的目的)。

可读性?

放下关于时间戳标准的争论,我认为我们不得不回顾下,为什么要在文本类格式中使用时间戳。

实际上使用时间戳的缺点是不言自明的,首先,它的精度只到秒,如果你希望表达一个更精确的时间那就不得不做类似前文中提到的 Java 以及 JavaScript 中 API 的行为。

其次,人类不可读。我想我们之所以使用类似 JSON 这样的格式传输数据,可读性应该占据了比较大的比重,其次就是简单的格式兼容性更好一点。事实上 JSON 是一种设计得非常差的格式,不仅体积庞大,还难以人工编辑。而在 JSON 中使用时间戳呢,1693730046,这是写这篇文章时的时间,你能一眼告诉我这是何年何月吗?

难以人工编辑是相较于那些专为人工编辑的格式,比如 TOML 哪怕是新的 JSON5,都提供了忽略末尾逗号和注释的能力,这对解析器来说并不是什么难以做到的事

我就不提 JSON 没有对数值类型的精度给出明确定义,很多程序以双精度浮点数进行处理而导致的丢失精度问题了,即使是以毫秒表达的时间戳,要丢失精度也要等到 2255 年。(但请注意有些地方会以纳秒表达,这就要出问题了)一旦真的遇到这个问题,很多平台提供的不可扩展的 JSON 解析库将导致问题非常难以解决。

争论

总有些人从一些奇怪的角度以一些站不住脚的观点来反驳:

  • “我用时间戳,效率更高”

    在现代计算机上,都已经用 JSON 了,就不要再扣这一点效率了(不管是空间上的还是时间上的)。你有这个时间,不如考虑考虑要不要换成 Protobuf。

  • “我用时间戳,运算时只需要简单的在数值上加减即可”

    现代编程语言都提供了强大的日期时间函数库,你没有任何理由不去使用而是直接在一串数字上加加减减,不管是从编程的便利性上,还是工程的规范性上(使用一个原始的 Long 变量意味着失去类型系统的保护)。给你个时间戳,要一个月后的时间,你看着加吧!

    有人听不懂,我解释一下:我不是让你对字符串进行操作,是让你对 java.time.Instanttime.Time 进行操作。不要直接操作时间戳,更不能直接操作时间字符串。(有人想往后面堆个 Z 解决部分库默认输出 LocalDateTime 的问题,吓死人了)

    温馨提示:传输使用的格式,和程序内部如何处理时间是完全不相干的两回事,唯一的交点是传输时的序列化/反序列化

  • “我用时间戳,不需要考虑时区”

    这本来是我想强调的另一个问题,但考虑到文章的标题,我不得不限制讨论的范围,免得又臭又长。简而言之一句话,不用时间戳,只要你别作死,一样不需要考虑。

我相信对于一个有基本工程素养的工程师来说,这都是些伪问题。

能怎么办?

实际上,关于时间的标准已经有好几份了,从 ISO 8601RFC 3339。这些标准化的格式,不论是在可读性上,还是严谨程度上都不是盖的。从工程角度上讲,Java 中的 java.time.Instant java.time.OffsetDateTime,Go 中的 time.Time 默认的 JSON 序列化/反序列化格式都符合这些标准,大部分 SQL 数据库也可以无障碍输入输出;相反时间戳,或者某些人自创的格式反而有各种各样的问题。

ISO 8601RFC 3339 都是相对宽松的格式,它们都允许丰富的变体(数数标准中有多少 MAY),而很多函数库默认接受的输入输出都是其中的一个子集,最完整的子集。一般来说最好去使用明确列举在 RFC 3339 的 ABNF (事实上引用的 ISO 8601)的表示方法,这也是大部分函数库的默认输出格式(日期时间中间使用 ‘T’ 分隔)。

关于时区

这里要批评阿里巴巴的 Java 手册,可找到毒瘤源头了(笑),不赘述,写在 issue 里了,可见 alibaba/p3c#983

简单来说,你不能以“我没有跨时区业务,所以直接用某个时区就行了”来解释自己为什么不在时间表答中明确时区。一个 2023-09-03 17:00:00 这样的字符串摆出来,鬼知道到底是什么时区?你假设大部分人都按 UTC 理解,这假设根本不成立,无数人当作 UTC+8 处理,但却真的有好多软件会给你按不一定哪来的时区处理。加个时区,累不死人,也多占用不了多少硬盘、网络、CPU。

去掉时区的“当地时间”,只有在你明确的要表达是时区无关的“当地时间”时才可以使用。

总结一下,我是说请使用 2023-09-03T23:00:00+08:00 或者 2023-09-03T18:00:00Z 作为传输格式。

查了下 Git 记录,我发现我大概是在21年底关掉页面的,但在那很久之前我应该就忘记了这里还有个blog了呢。那时候大概是觉得,没什么可写的,我无法输出什么有价值的东西,完全无法和那些大佬的blog比,自娱自乐过一阵也就累了。

时隔两年,离开了学校,现在也得一个人面对本来就没什么希望的人生了😥,我突然发现我好像还是需要一个笔记本的。
那么就这样吧,去掉那些华而不实的主题,不再想花时间在折腾hexo主题上,把之前的这个blog重新部署上,就当作一个笔记本吧。

要准备的东西

  • STM32CubeIDE
  • OpenOCD
  • CMSIS-DAP 调试器
  • STM32开发板(我用的STM32F103)

步骤

去Cube里开个项目,生成代码时注意要在 Pinout & Configuration / System Core / SYS 中把 Debug 调整成 Serial Wire

运行 openocd -f interface/cmsis-dap.cfg -f target/stm32f1x_stlink.cfg (注意要确保线路接好,且调试器/开发板 都已经上电运行,否则openocd起不来)

OpenOCD是一种开源调试方案,除此之外还支持大量目标平台和调试器,可在 script 目录下查看

回到CubeIDE,在 Run / Debug Configuration… 中添加一个Debug配置,注意GDB端口要和openocd的端口一致

image-20200926203922007

要确保线路接好, 否则出各种诡异问题emmm,如果调试迟迟不能成功,尝试手动复位,应该可看到OpenOCD输出诸如 Info : stm32f1x.cpu: external reset detected 字样

关于 Kotlin Coroutine 的使用就不多说了,大家都已经很熟悉emmm,这里简单探索下 Coroutine 内部的实现,来尽量规避Coroutine 与其他库和框架协同使用时的坑。

本文编写时使用 Kotlin 版本:1.4-M3,JDK 版本:openjdk 11.0.7

Coroutine 的核心原理 KEEP 中的 coroutine 提案已经写得很清楚了,详见提案的实现详情章节: KEEP/proposals/coroutines.md#implementation-details ,这里补充一点点提案中没说的,和实现高度相关的内容。

本质就是对 suspend 函数进行CPS变换,将代码转换为延续体传递风格。简单来说就是为每个函数增加了一个隐式的参数 $completion,它的类型是 Continuation 即延续体;函数本体则被编译成状态机,状态存储于上述的 Continuation 中,suspend 函数每次 resume 时都会被调用,其根据 Continuation 中的状态信息直接调转到对应的位置继续执行。

为了便于分析,这里使用 CFR 反编译器对 kotlinc 编译的 suspend 函数进行反编译,当前版本为 0.150

我们从下面代码段开始:

1
2
3
4
5
6
7
class Demo {
suspend fun demo1(s: String) {
println("1==========")
delay(100)
println("2==========")
}
}

注意,Kotlin 编译器对其进行编译后我们看到两个 class Demo$demo1$1.class Demo.class 前面那个是后面的匿名内部类,不用管它,用 cfr 反编译后面的就行了。以下我将只节选部分源码,注意这不是标准的Java代码,没法直接编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public final class Demo {
/*
* Unable to fully structure code
* Enabled aggressive block sorting
* Lifted jumps to return sites
*/
@Nullable
public final Object demo1(@NotNull String s, @NotNull Continuation<? super Unit> $completion) {
if (!($completion instanceof demo1.1)) ** GOTO lbl-1000
// 这个 demo1.1 就是下面new出来的那个ContinuationImpl
var6_3 = $completion;
if ((var6_3.label & -2147483648) != 0) {
var6_3.label -= -2147483648;
} else lbl-1000:
// 2 sources

{
$continuation = new ContinuationImpl(this, $completion){
/* synthetic */ Object result;
int label;
final /* synthetic */ Demo this$0;
Object L$0;
Object L$1;

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return this.this$0.demo1(null, (Continuation<? super Unit>)this);
}
{
this.this$0 = demo;
super(continuation);
}
};
}
$result = $continuation.result;
var7_5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch ($continuation.label) {
case 0: {
ResultKt.throwOnFailure((Object)$result);
var3_6 = "1==========";
var4_7 = false;
System.out.println((Object)var3_6);
$continuation.L$0 = this;
$continuation.L$1 = s;
$continuation.label = 1;
v0 = DelayKt.delay((long)100L, (Continuation)$continuation);
if (v0 == var7_5) { // if result == COROUTINE_SUSPENDED
return var7_5; // return COROUTINE_SUSPEND
}
** GOTO lbl27 // else goto lbl27
}
case 1: {
// 状态机在这里 resume,先恢复局部变量
s = (String)$continuation.L$1;
this = (Demo)$continuation.L$0;
ResultKt.throwOnFailure((Object)$result);
v0 = $result;
lbl27:
// 2 sources
// 如果一开始就没有挂起,那自然也就不需要恢复咯
var3_6 = "2==========";
var4_7 = false;
System.out.println((Object)var3_6);
return Unit.INSTANCE;
}
}
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}

上面这个很清晰了,这个 suspend 真正被调用时会创建一个 ContinuationImpl 的子类,里边存放了状态机的状态 label 和局部变量,还有最关键的,传进来的调用者的延续体也被包含在了里面,这个稍后会用到。

当协程挂起时,状态机函数会返回 COROUTINE_SUSPEND 这个对象,这也就是 suspend 函数编译后函数返回值必然为 Any 的原因,实际的返回值是 返回值 T 和 COROUTINE_SUSPEND 之一,显然这是在 Java 和 Kotlin 类型系统中均无法表达的。局部变量等状态信息,都会在调用下一个 suspend 函数前保存进延续体 (45~47)

接下来看一看 CoroutineImpl 这个类到底干了什么,这个类的代码很多,我不全粘过来了,只节选有意义的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public final override fun resumeWith(result: Result<Any?>) {
// This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
var current = this
var param = result
while (true) {
// Invoke "resume" debug probe on every resumed continuation, so that a debugging library infrastructure
// can precisely track what part of suspended callstack was already resumed
probeCoroutineResumed(current)
with(current) {
val completion = completion!! // fail fast when trying to resume continuation without completion
val outcome: Result<Any?> =
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
// unrolling recursion via loop
current = completion
param = outcome
} else {
// top-level completion reached -- invoke and return
completion.resumeWith(outcome)
return
}
}
}
}

其实这个函数做的事很简单,不停的循环并调用 current 指针的 invokeSuspend 函数来恢复协程的执行,如果返回 COROUTINE_SUSPEND 那就意味着又是暂停,返回;否则说明当前函数执行完了,从 current 指针指向的延续体中拿出它上一级的延续体,继续 invoke,直到回到根,结束退出。

总结下,传入每个 suspend 函数的延续体在初始时都是调用者的延续体,当 resume 时会传入本函数的延续体,并根据里边的 label 去往相应的状态,同时协程暂停时会返回那个特殊的 COROUTINE_SUSPEND 对象。

可能和 Git for Windows 在安装时的选项有关,需要注意默认情况下 git 可能不会使用 Windows 中安装的 gpg.exe,这必然会导致找不到key。(要不是因为看到了gpg的初始化输出我打死也想不到是这个问题)

git config --global gpg.program "c:/Program Files (x86)/GnuPG/bin/gpg.exe" 即可,注意看准了自己机器上的路径是啥。

git config commit.gpgsign true 即可设置该仓库默认签名提交,IDEA里 commit 时就会弹出 Git for windows 的密钥窗口了。

我也不会写什么blog,那么,这里就当作一个笔记本用吧,毕竟有些东西,好不容易搞明白,忘记了还是很麻烦的呢。
要是能借机会帮到其他人,我也是很高兴的呢。

这里用Hexo驱动,Github 托管 + Cloudflare CDN 外加一份开源的主题,自己凑凑合合写个Github Action workflow(其实没有也无所谓啊),就算差不多能用了。
首页的链接嘛,基本都点不开(x

那么,就先这样了,最后,忘了说一句,既然你来了,那说明咱俩有缘啊(x

总之,欢迎光临了)x

满口胡话的iseki

2019.10.17

不想阅读Github Action厚重的文档,用预配置好的NPM Action折腾了半天,终以失败告终。由于不熟悉NPM和Node.js,最后爆出的错误摸不到头脑,就此作罢。

寻找另一个方法,在 https://github.com/user/repo/actions/new 中选择了跳过,自己设置一个空白的Workflow。

  1. 为了避免Action重新推送仓库后循环触发Action(不知Github有没有对这种情况进行特殊处理)添加路径过滤器,仅当 /source 目录存在更新时才触发 Workflow

  2. 在仓库的 Settings->Deploy keys 里设置Github Action push时使用的SSH公钥,并赋予写权限,Settings->Secrets 里设置私钥的 Base64 编码。
    私钥会通过环境变量传入Action的shell,base64 -d 即可解码,base64编码是为了避免潜在的回车换行符问题(环境变量里出现的换行符似乎不能正确地写入文件)

  3. 注意工作目录的问题,别瞎跑,目录会丢掉的

  4. ~/.ssh/id_rsa 文件注意设定权限 0600 默认的权限过于宽松,SSH会不读取。

  5. ~/.ssh/config 里需写入

    1
    2
    Host *
    StrictHostKeyChecking no

    否则可能会弹出要求确认SSH fingerprint的交互消息。

  6. 记得设定 git config --global user.name/email 否则无法提交

  7. 如果使用了自定义的域名,注意 CNAME 文件是否在 hexo clean 后被删除,可能需要自己写回去。

  8. 注意部分Hexo主题可能以 git submodule 方式引入, Github Action 克隆仓库时不会自动克隆子模块,导致生成的所有页面空白,Hexo只会给出警告而不是错误。

附上自己写的workflow(糊成一坨,凑合能用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
name: Generate Hexo

on:
push:
paths:
- 'source/**'

jobs:
build:
name: Refresh
runs-on: ubuntu-latest
steps:
- name: Install Node.js and NPM
run: |
sudo apt-get install curl -y
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install nodejs -y
echo '=====Show NPM version====='
npm -v
echo '=====Show Node.js version====='
node -v
- name: Install Hexo
run: |
sudo npm install -g hexo-cli
echo '=====Show HEXO version====='
hexo -v
- name: Prepare Key and SSH config
run: |
mkdir ~/.ssh
echo $DEPLOY_PRIVKEY | base64 -d > ~/.ssh/id_rsa
chmod 0600 ~/.ssh/id_rsa
echo 'Host *' >> ~/.ssh/config
echo ' StrictHostKeyChecking no' >> ~/.ssh/config
chmod 0600 ~/.ssh/config
env:
DEPLOY_PRIVKEY: ${{ secrets.DEPLOY_PRIVKEY }}
- name: Clone git repo
run: |
cd ~
echo '=====Show work path====='
pwd
git clone git@github.com:cpdyj/blog.git
cd blog
ls
echo '=====Show work path====='
pwd
cat ./package.json
npm install
npm audit fix
hexo clean
hexo g
echo "blog.iseki.space" > ./docs/CNAME
git config --global user.name "User name"
git config --global user.email "Email@example.com"
git add docs
git commit -am "Auto generate on Github Action at `date`"
git push
0%