- 投稿日:
undefinedの判定方法が複数あるということでundefined判定の処理速度比較をしてみたのでその結果。
端的に言うと、hoge === undefined
とtypeof hoge === 'undefined'
の二方式がある。後者は原則考慮不要だが、言語仕様上存在しているので比較したが、現実的に見た場合、どちらで記述した場合でも処理速度に有意な差はないように感じた。
確認環境
Env | Ver |
---|---|
Node.js | 20.1.0 |
TypeScript | 4.9.5 |
@swc/core | 1.3.8 |
比較結果
hoge === undefined
の方が早く見えるが実行するタイミングで変わるので誤差の範疇だと思う。
方式 | ms |
---|---|
hoge === undefined |
4,514 |
typeof hoge === 'undefined' |
4,515 |
確認コード
const tyof = (param?: string) => {
return typeof param === 'undefined';
};
const undef = (param?: string) => {
return param === undefined;
};
const tyStart = +new Date();
for (let i = 0; i < 10000000000; i++) {
tyof();
}
console.log('typeof', +new Date() - tyStart);
const unStart = +new Date();
for (let i = 0; i < 10000000000; i++) {
undef();
}
console.log('undefined', +new Date() - unStart);
TSから生成されたJS
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
const tyof = (param)=>{
return typeof param === 'undefined';
};
const undef = (param)=>{
return param === undefined;
};
const tyStart = +new Date();
for(let i = 0; i < 10000000000; i++){
tyof();
}
console.log('typeof', +new Date() - tyStart);
const unStart = +new Date();
for(let i = 0; i < 10000000000; i++){
undef();
}
console.log('undefined', +new Date() - unStart);
あとがき
MDNを読む限りtypeof hoge === 'undefined'
は該当変数が存在しない場合に有用なようであるが、TypeScriptで書いている場合、通常このようなコードが生まれることがなく、仮に起きるとした場合、次のようなコードになるため現実的に考慮する必要はない。なおMDNにも「こんなことはしないこと」と書いてあるので、一般的なコードでないことは客観的にも伺えるだろう。
(() => {
const undefined = 123;
const hoge = undefined;
if (typeof hoge === 'undefined') {
console.log('hoge is undefined');
} else {
console.log('hoge is not undefined');
}
})();
上記コードの実行結果としてはhoge is not undefined
が出力される。
このコードの主な問題点
const undefined = 123;
というコードは予約語を変数名にしているため、混乱を招くコードであり、書かないことが好ましい- MDNには予約語ではないとあるが、一般的には予約語の一つとして解釈して支障ないと考える
- このコードはESLintのeslint:recommendedで検知されるため、通常であれば書かれることはない
なお、このコードは例示のために即時実行関数形式で記述しているが、必要がない限りこの形式での実装は避けたほうが問題が少なくなると思う。これは不必要なネストが生まれたり、スコープの混乱を生むためである。
- 投稿日:
HuskyとはNode.jsを利用した開発で非常によく使われているGit hooksのユーティリティだが、個人的にはこのツールの存在価値に疑問を感じている。
という訳で、この記事ではHuskyのメリデメを考えた結果と、Huskyが何をしているか、Huskyの必要性について書いてゆく。
Huskyがあるメリット
Huskyは極めて有名なツールであるため、Huskyが入っているとこのプロジェクトはGit hooksで管理されており、標準化されていることが確認できるだろう。恐らくHuskyのメリットはこれ以外に存在しないと考えている。
Huskyがあるデメリット
Huskyの管理をしないといけない、Huskyも地味にアップデートするからだ。これは明確なコストである。
そしてHuskyのコードやリポジトリを見たことがある人は多分ほとんどいないと思う。更に言えばHuskyが何をしているかすらも知らない人だっているはずだ。そんな得体の知れないものを使うのは怖いというところだ。
Huskyは何をしているか?
端的に言えばGit hooksのパスを .husky/
に設定しているだけである。
要するに git config core.hooksPath .husky
を叩いているだけだ。
もう少し細かく言えば以下に相当する処理を実行している。
mkdir -p .husky/_
cp husky.sh .husky/_
git config core.hooksPath .husky
勿論、ソースコードには他の処理も書かれているのだが、実質的には上記三行が全てと言って良い。
husky.sh
を活用しているケースがどれほどあるか怪しいことを考えると、本質は git config core.hooksPath .husky
だと思うので、正直あるだけ邪魔では?と考えている。
Huskyの必要性
ここまででHuskyがしていることは git config core.hooksPath .husky
だということが解ったが、だとしたらHuskyは本当に必要なのだろうか?私は特に理由がないのであれば package.json
で husky install
と書いてあるところに git config core.hooksPath .githooks
とでも書いておけば良いのではないか?と思っている。恐らく何も不都合はないはずだ。
ただ世の中には色々な事情があり、使わざるを得ないケースもあると思う。しかし、可能であれば排除してもいいのではないか?個人的にはそう思っている。
何故この記事を書いたか
「この世からHuskyを滅ぼすため」というのはまぁ冗談だが、個人的にHuskyの存在価値があまり良くわかっておらず、多分世間の人もあまり理解できていないと勝手に考えていて、可能であればプロジェクトに入れたくないと考えているので、そのお気持ち表明というか、そんな感じだ。
ここからは余談だが、Huskyには結構な数のスポンサーが付いていて、恐らく毎月それなりの収入があると思われる。以下はHuskyのスポンサーである。
個人的にHuskyは最も成功したOSSの一つではないかと考えている。理由としてHusky自体は非常に単純なプロダクトであり、コミット履歴を見てもさしたるメンテナンスがされておらず、ほぼ手放しで維持されていると思われるからだ。
しかし、Huskyはそれなりの額の寄付を集めており、この記事を書いた時点で確認できるだけでも最低 10USD * (4 + 16) + 100USD * (4 + 2)
の寄付がされており、つまり800USD、日本円にして11.2万円ほどだ。何もしてないのに毎月この収入があるのは大分ありがたいだろう。他のOSSならIssueやPull Requestsに対して対応したり、コード本体のメンテナンスがあるはずだが、Huskyにそんなものはないため、プロダクトの維持コストに対して非常によく寄付を集められていると感じる。
- 投稿日:
TypeScript + SWCと組み合わせてJestを回してるとテストケースの増加に伴いメモリリークが発生する現象が起きます。メモリに余裕があれば問題にはならないですが、開発機のメモリが足りないとかCIで使ってるECSのメモリが足りないとか、引っかかるケースもあると思います。
今回はこの問題に対する対処法を書いていきます。完全には改善されませんが、かなりマシになります。参考までに今回の検証では283MB使ってたのが67MBまで減りました。
この現象はNode.js 16.11.0以降で発生するらしいので、それ以前の環境では起きないかもしれません。
確認環境
Node.js v20.0.0
module | version |
---|---|
@swc/cli | 0.1.62 |
@swc/core | 1.3.59 |
@swc/jest | 0.2.26 |
@types/jest | 29.5.1 |
@types/node | 20.2.3 |
jest | 29.5.0 |
jest-watch-typeahead | 2.2.2 |
typescript | 5.0.4 |
確認用のテストコード
以下のテストコードを書いたファイルを100ファイル作り、それを流して確認しています。
describe('example1', () => {
it('1', () => {
expect(1).toBe(1);
});
it('2', () => {
expect(2).toBe(2);
});
it('3', () => {
expect(3).toBe(3);
});
it('4', () => {
expect(4).toBe(4);
});
it('5', () => {
expect(5).toBe(5);
});
});
describe('example2', () => {
it('1', () => {
expect(1).toBe(1);
});
it('2', () => {
expect(2).toBe(2);
});
it('3', () => {
expect(3).toBe(3);
});
it('4', () => {
expect(4).toBe(4);
});
it('5', () => {
expect(5).toBe(5);
});
});
確認環境一式
以下のリポジトリに確認したソースコードを一式格納しています。
[blogcard https://github.com/Lycolia/jest-memory-leak-example]
メモリリークしていく様子
(41 MB heap size)
(42 MB heap size)
(52 MB heap size)
(51 MB heap size)
(37 MB heap size)
(47 MB heap size)
(47 MB heap size)
(50 MB heap size)
(60 MB heap size)
(60 MB heap size)
(62 MB heap size)
(71 MB heap size)
(73 MB heap size)
(74 MB heap size)
(84 MB heap size)
(85 MB heap size)
(86 MB heap size)
(96 MB heap size)
(97 MB heap size)
(99 MB heap size)
(108 MB heap size)
(110 MB heap size)
(111 MB heap size)
(120 MB heap size)
(122 MB heap size)
(124 MB heap size)
(75 MB heap size)
(83 MB heap size)
(84 MB heap size)
(86 MB heap size)
(96 MB heap size)
(96 MB heap size)
(97 MB heap size)
(107 MB heap size)
(108 MB heap size)
(109 MB heap size)
(118 MB heap size)
(120 MB heap size)
(121 MB heap size)
(130 MB heap size)
(132 MB heap size)
(133 MB heap size)
(143 MB heap size)
(144 MB heap size)
(145 MB heap size)
(154 MB heap size)
(156 MB heap size)
(157 MB heap size)
(166 MB heap size)
(168 MB heap size)
(169 MB heap size)
(179 MB heap size)
(181 MB heap size)
(182 MB heap size)
(191 MB heap size)
(193 MB heap size)
(194 MB heap size)
(203 MB heap size)
(205 MB heap size)
(207 MB heap size)
(216 MB heap size)
(217 MB heap size)
(219 MB heap size)
(228 MB heap size)
(230 MB heap size)
(231 MB heap size)
(240 MB heap size)
(242 MB heap size)
(243 MB heap size)
(252 MB heap size)
(254 MB heap size)
(255 MB heap size)
(264 MB heap size)
(266 MB heap size)
(267 MB heap size)
(277 MB heap size)
(278 MB heap size)
(280 MB heap size)
(195 MB heap size)
(204 MB heap size)
(203 MB heap size)
(212 MB heap size)
(213 MB heap size)
(214 MB heap size)
(223 MB heap size)
(224 MB heap size)
(226 MB heap size)
(235 MB heap size)
(237 MB heap size)
(238 MB heap size)
(247 MB heap size)
(249 MB heap size)
(250 MB heap size)
(259 MB heap size)
(261 MB heap size)
(262 MB heap size)
(271 MB heap size)
(273 MB heap size)
(274 MB heap size)
(283 MB heap size)
Test Suites: 100 passed, 100 total
Tests: 1000 passed, 1000 total
Snapshots: 0 total
Time: 11.347 s, estimated 12 s
解消方法
npm i -D @side/jest-runtime
などで@side/jest-runtimeを導入し、jest.config.js
に以下の行を追加することで改善します。
runtime: '@side/jest-runtime',
このコードで何か既存の実装やテストに影響が発生するかどうかは確認していませんが、後述する@side/jest-runtimeが生まれる切欠になったPRの様子を見る限り大丈夫なんじゃないかなとなんとなく思っています。
メモリリークが改善したあとの様子
(39 MB heap size)
(39 MB heap size)
(47 MB heap size)
(47 MB heap size)
(34 MB heap size)
(42 MB heap size)
(41 MB heap size)
(49 MB heap size)
(51 MB heap size)
(59 MB heap size)
(59 MB heap size)
(60 MB heap size)
(68 MB heap size)
(69 MB heap size)
(77 MB heap size)
(77 MB heap size)
(85 MB heap size)
(86 MB heap size)
(95 MB heap size)
(95 MB heap size)
(95 MB heap size)
(103 MB heap size)
(104 MB heap size)
(112 MB heap size)
(112 MB heap size)
(120 MB heap size)
(121 MB heap size)
(31 MB heap size)
(38 MB heap size)
(37 MB heap size)
(44 MB heap size)
(46 MB heap size)
(54 MB heap size)
(54 MB heap size)
(54 MB heap size)
(62 MB heap size)
(62 MB heap size)
(70 MB heap size)
(71 MB heap size)
(79 MB heap size)
(79 MB heap size)
(87 MB heap size)
(88 MB heap size)
(96 MB heap size)
(96 MB heap size)
(97 MB heap size)
(105 MB heap size)
(105 MB heap size)
(31 MB heap size)
(39 MB heap size)
(37 MB heap size)
(44 MB heap size)
(46 MB heap size)
(54 MB heap size)
(54 MB heap size)
(54 MB heap size)
(62 MB heap size)
(62 MB heap size)
(70 MB heap size)
(70 MB heap size)
(78 MB heap size)
(79 MB heap size)
(87 MB heap size)
(88 MB heap size)
(96 MB heap size)
(96 MB heap size)
(96 MB heap size)
(104 MB heap size)
(105 MB heap size)
(31 MB heap size)
(39 MB heap size)
(37 MB heap size)
(45 MB heap size)
(46 MB heap size)
(54 MB heap size)
(54 MB heap size)
(54 MB heap size)
(62 MB heap size)
(63 MB heap size)
(70 MB heap size)
(71 MB heap size)
(78 MB heap size)
(79 MB heap size)
(87 MB heap size)
(88 MB heap size)
(96 MB heap size)
(96 MB heap size)
(97 MB heap size)
(105 MB heap size)
(105 MB heap size)
(30 MB heap size)
(37 MB heap size)
(45 MB heap size)
(43 MB heap size)
(44 MB heap size)
(51 MB heap size)
(51 MB heap size)
(59 MB heap size)
(59 MB heap size)
(67 MB heap size)
Test Suites: 100 passed, 100 total
Tests: 1000 passed, 1000 total
Snapshots: 0 total
Time: 9.071 s, estimated 11 s
参考記事
メモリリークバグに関するIssue
https://github.com/jestjs/jest/issues/11956
@side/jest-runtimeが出来る元になったPR
https://github.com/jestjs/jest/pull/12205
- 投稿日:
Windows向けのツールを作ろうというので、昔よく使っていたC#.NETで開発をすることにしたのですが結構苦労があったのでその話を書いていこうと思います。Unit testingを書くのは一旦断念しましたが、暇があれば挑戦したいとは思ってます。
事のいきさつ
WindowsのVSCodeでRemote Development拡張を使うことが多いのですが、リモートエクスプローラーにアイテムが増えてくると移動したいフォルダを探すのが大変になってくるのと、ここに出てないフォルダを開くのが結構手間という問題があり、これを解決できないかということを考えていました。
そこで考えついたのがWSLやSSHのネットワークパス上でExplorerの右クリックメニューを開いたらVSCodeがリモートモードで起動できると良いのでは?という所でした。Windows向けでexeからバイナリ起動するならC#で書くのが楽だろうと考え、C#での開発に着手しました。
疎結合な実装でUnit testingもできたらいいなーと漠然と考えながら開発していたのですが、最後にC#を書いたのは3年ほど前で、C#でモダンな実装をした経験もなかったので結構苦労しました。よく考えたらTypeScriptかJavaScriptくらいでしかまともにやったことがないし、UT自体他言語でもGolangかPHPでしか書いたことがなかったので、C#でこれをやるのは自分の実装技術に対する一種の挑戦みたいなところがありました。結論としてはあっけなく破れたわけですが…。
ツールの要件
- Explorerの右クリックメニューから起動する
- 開いたパスに応じてWindowsローカル、SSH、WSLを判定し、適切なモードでVSCodeを起動する
- パス解決には設定ファイルを用いる
- Explorerから渡されるパスはリモート環境のパスと差異があるので、そこを解決するためのものです
ツールの設計
やることは設定ファイルを読み込み、コマンドライン引数から値を取得し、それらをいい感じに変換してVSCodeに渡すだけです
大まかなフロー
実行形式
- コマンドライン引数
this.exe <dir path>
-
設定ファイル形式
```json5
{
"CodePath": "VSCodeのexeパス",
// リモートホスト定義。辞書形式
"Remote": {
"リモートホスト名": {
"ExplorerPrefix": "explorerのパスから除外する文字列",
"AppendPrefix": "explorerのパスに追加する文字列"
},
}
}
```
ツールの実装
まずはTDD的にやろうとしてInterfaceを作成し、Classで実装し、ロジック自体は好調に組み上がっていき、テストもすべて通るようになりました。但し実際にexeを蹴ると動かず、テストは通るが動かないゴミが出来上がっていました。(途中で動作確認をしなかったのか?と思うかもしれませんが、実際は完全に動くものを作ってからテスト可能な形に書き換えるという流れで作っていたのでこうなっています)
動かなかった理由は設定ファイルにJSONを採用していて、デシリアライザにSystem.Text.Json.JsonSerializer
を使っていたためです。このデシリアライザは標準ではInterfaceに対してJSONをデシリアライズできません(考えてみれば当然ですが)。しかし、テストをするためにはモックをDIしたいので、Interfaceが必要です。
以下は実際に設定を読み込むために実装したClassですが、メンバが全部Interfaceになっているので、標準の状態ではデシリアライズに失敗します。ならばデシリアライザを自作すればいいという話が出てくるのですが、高々設定ファイルを読み込む処理にそこまで情熱を込めるか…?と言うことになり、諦めました。
public class ConfigBase : IConfigBase {
private IFileInfo? _CodePath;
private IDictionary<string, IConfigRemote>? _Remote;
public IFileInfo CodePath {
get {
if (this._CodePath == null) {
throw new Exception("Config Error: Missing CodePath.");
} else if (this._CodePath == null) {
throw new Exception("Config Error: CodePath is empty. Set the code.exe path in this field.");
} else if (!this._CodePath.Exists) {
throw new Exception("Config Error: CodePath is Not exists.");
} else {
return this._CodePath;
}
}
set { this._CodePath = value; }
}
public IDictionary<string, IConfigRemote> Remote {
get {
if (this._Remote == null) {
throw new Exception("Config Error: Missing Remote.");
} else if (this._Remote.Count == 0) {
throw new Exception("Config Error: Remote is empty. Set the remote infomation in this field.");
} else {
return this._Remote;
}
}
set { this._Remote = value; }
}
}
そもそもこのツールは「設定ファイルを読み込み、コマンドライン引数から値を取得し、それらをいい感じに変換してVSCodeに渡すだけ」のツールです。たったそれだけのツールに入れる仕組みにしては大げさすぎると感じました。
テストをするためにInterfaceを作ったり、本実装とモック用のClassを作る程度まではまだ容認できるのですが、カスタムデシリアライザを作ると、今度はそれのテストも必要になってきます。どう考えてもしんどい。
因みにこのコード、仮にInterfaceをやめても文字列をFileInfo
に組み替えるためのカスタムデシリアライザの実装が必要で結構頭が痛くなります…。多分そこはFile.exists
をラップしたクラスをDIしてやるのが無難な気がしますね。
この辺はTypeScriptだとimport
の中身をJestで書き換えたら終わりなのであんま考えなくていいのは楽ですが、C#だと厳しいなと感じました。
C#でテストを書いていて思ったこと
クラスベースで実装していくとメソッド単位でのテストがしづらく、テストがコケても原因が把握しづらいというのを一つ課題感として覚えました。TSでなんちゃって関数型開発をしていれば関数のUTを書けば関数の挙動を把握できますが、Classではそうも行きません。
仮に1つのpublic methodが5つのprivate methodを呼び出していて、private methodの裏ではprivate propertyが複雑な依存を持っていたとしたらどうでしょうか?
正直デバッグをかけないとどこで何がコケたか特定できないと思います。そんな実装にするのが悪いといえばそうですが、でもクラスってそういうものじゃないですっけ…?お互いに関連性がなくていいならもうそれ関数で良くないですか?って思いました。
勿論、全部静的メソッドにしてClassそのものは単なるエンティティにするのも選択肢の一つだとは思います。しかしC#でそこまでやるか…?という疑念が個人的にあるのと、処理を繋げたテストをUTとして書く方法がなくなると思います。例えばインスタンスメソッドならモックをDIすることでメソッドが呼ばれた事や、戻り値に対する分岐を確認できますが、静的メソッドでこれをやるのは難しいと思います。
Classの単体テストは関数と状態が密結合したテストになってしまうので、いまいち微妙だなと思ったのが今回思ったことでした。
- 投稿日:
runas
では上手く出来ないっぽいのでPowerShellを呼んで実行する。管理者実行したいコマンドを分けたい時に活用できる。
確認環境
Env | Ver |
---|---|
Windows 11 Pro | 22621.1413 |
サンプルコード
powershell start-process <ここにコマンド> -verb runas
備考
PowerShellのリファレンスに沿って書くなら大文字小文字を書き分ける方がより正しいが、基本的に大文字と小文字を区別しないため、どっちでも動く。
powershell Start-Process <ここにコマンド> -Verb runAs