非推奨になったESLintのルールがESLintのコード上でそう記されているか確かめるESLintのテストを書いた
ESLintのレポジトリでは、lib/rules
ディレクトリ配下にルールがまとまっている。
以下に記されているように、それらのルールは非推奨になることがあるが、削除されることはない。
Rule Deprecation - ESLint - Pluggable JavaScript Linter
Balancing the trade-offs of improving a tool and the frustration these changes can cause is a difficult task. One key area in which this affects our users is in the removal of rules.
...
Rules will never be removed from ESLint.
非推奨となったルールは、そのコードとドキュメントにその旨が記されている。
例えば、ESLint v7.0.0で非推奨となったcallback-return
というルールでは、lib/rules/callback-return.js
とdocs/rules/callback-return.md
でそれぞれ以下のような記述がある。
/** * @fileoverview Enforce return after a callback. * @author Jamund Ferguson * @deprecated in ESLint v7.0.0 */ ...
This rule was **deprecated** in ESLint v7.0.0. Please use the corresponding rule in [`eslint-plugin-node`](https://github.com/mysticatea/eslint-plugin-node).
しかし、コードとドキュメントにこのようにきちんと記されているか確認するフローがなかったため、記述が抜け漏れている箇所があった。
非推奨になっているルールの一覧が Rules - ESLint - Pluggable JavaScript Linter にあるので、それを目視で確認しながら修正していった。
Chore: Update deprecated information by wafuwafu13 · Pull Request #14961 · eslint/eslint · GitHub
このPRで、「非推奨になったルールのコードに@deprecated
タグがあること」、「README.md
にThis rule was deprecated...
の記述があること」を確かめるテストがあればよいというレビューがあった。
「目的のファイルに目的の記述があること」は、ShellJSのcatと正規表現を使って、すでに同じようなテストをしている箇所があったので書けそうだった。
eslint/Makefile.js at 80cfb8f858888bddfefd7de6b4ecbf5aabe267bc · eslint/eslint · GitHub
function hasIdInTitle(id) { const docText = cat(docFilename); const idOldAtEndOfTitleRegExp = new RegExp(`^# (.*?) \\(${id}\\)`, "u"); // original format const idNewAtBeginningOfTitleRegExp = new RegExp(`^# ${id}: `, "u"); // new format is same as rules index return idNewAtBeginningOfTitleRegExp.test(docText) || idOldAtEndOfTitleRegExp.test(docText); }
問題は、「ルールが非推奨になっていること」をどのように定義するかだった。
先ほど述べたように非推奨になっているルールの一覧はRules - ESLint - Pluggable JavaScript Linterにあるのだが、それをfetch等でとってくるのは、コードが煩雑になりそうなのと、ルールの一覧のフォーマットやページが変わることによってテストが落ちるので避けたかった。
非推奨になったルールの一覧を写したjsonファイルを作る手もあったが、そのjsonがきちんと更新されなければ元も子もない。
ESLintのリリースフローはよく知らないが、このルールの一覧もコードから生成されているため、どこかにフラグがあるに違いないと思い探していると、メンテナーのPRが見つかった。
Update: deprecate Node.js & CommonJS rules by kaicataldo · Pull Request #12898 · eslint/eslint · GitHub
そこでは、各ルールのコードで書かれているドキュメントのような箇所でdeprecated: true,
が追加されていた。
module.exports = { meta: { deprecated: true, ...
これはおそらくリリースフローに組み込まれているため、非推奨のルールが抜け漏れている心配はない。
よって、以下のようなコードで、非推奨になったESLintのルールがESLintのコード上でそう記されているか確かめるテストがかける。
function hasDeprecatedInfo() { return 「非推奨になったルールのコードに@deprecatedタグがあること」 && 「README.mdにThis rule was deprecated...の記述があること」 } if (ruleDef.meta.deprecated && !hasDeprecatedInfo()) { エラー++ }
今朝approvedされた。
Chore: Add test that deprecated rules display a deprecated notice by wafuwafu13 · Pull Request #14989 · eslint/eslint · GitHub
ミクシィのインターンで みてね の開発に幅広く関われました
https://mixigroup-recruit.mixi.co.jp/recruitment-category/internship/5071/ に8/2~8/27の約1ヶ月間参加しました。
選考は、インフラ/SRE領域に普段あまり触れていなかったため、その枠にチェックを入れて書類を提出しました。
3回の面接を通じて、今まで自分がやってきたこと等も考慮し、みてね のアプリ開発チームを主体に、SREチームにも関わるという形での就業となりました。
勤務形態としては前半2週間が東京、後半2週間が京都、前半3週間がアプリ開発チーム、後半1週間がSREチームという形で動きました。
アプリ開発チーム
アプリ開発チームでは複数のプロジェクトが同時進行で動いていて、それぞれのチームでiOS, Android, Railsの開発を進めていくという体制が取られていました。
自分はその中のひとつのプロジェクトに入って、そのプロジェクトの機能を少し拡張するために、モデルの関連の追加、管理画面の作成、今後の方針を立てる、ということをしました。
時間あたりの作業量は普段より少なく、難易度も至って普通でしたが、単独したタスクや既存の改善ではなく、新しくプロジェクトが進んでいる段階でモデルの作成から入る経験は少なかったため、開発のフローやスピード感がリアルに感じれました。
メンターの方が2年目だったため、仕事のイメージも湧きやすく、直接話も聞けたのでよかったです。
モバイルの画面を作ったりすることはなかったですが、リファクタリングのタスクが落ちていたので、方針に沿ってリファクタ作業もしました。
Swiftを実務で使うのは初めてで、設計まで踏み込んで読めたのはよかったです。
モバイルからサーバーまでやっているチームと聞いて、主にモバイルをやってサーバーはちょっとした変更に留まっているようなイメージを持っていましたが、DB設計からモバイルのアーキテクチャ設計まで思っていたよりガッツリやられていました。
1ヶ月という期間もあり、自分は0=>1のDB設計の段階の少しに関わったり、あらかじめ方針が固まったリファクタをしたに過ぎないですが、普段から両方に目をやれる体制が自分には合っているなと感じました。
チーム内でiOS, Android, Railsの足並みを揃えるのもスムーズにやられているように感じたのと、デザインに関しては みてね のチーム全体でより良いものに改善していく取り組みがなされていたので、そこの体制も良いなと思いました。
あと単純にどんどん新しい施策が0から生まれていってる環境は楽しいなと感じました。
主なリファレンス
- Active Record の関連付け - Railsガイド
- オブジェクト指向設計実戦ガイド/6.4 抽象を見つける
- SQLアンチパターン/6.5 解決策:関連(リレーションシップ)を単純化する
- iOSアプリの設計にClean Architectureを採用して約3年運用してきた知見 | by ロクネム | MIXI DEVELOPERS
- iOSアプリ設計パターン/3.3 抽象に依存し、再利用性を向上する
- みてねの開発スタイルを大きく変えた話 - Atsushi Sakai - Medium
SREチーム
1スプリントを みてね のSREメンバーと一緒に朝会から参加させていただきました。
アプリ開発チームにいたときには見えなかったタスクの内容や動き方が知れてよかったです。
インフラに普段あまり触れていないのとチームのレベルがとても高い分、人や技術に対して尖った印象があったのですが、温かく迎え入れてくださり、終始ペアプロもしてくださったのでとても助かりました。
タスクの内容としては、「IP不足の問題が起きたことがあったため、EKSのCNI Metrics Helperを使って、割り当てられた IP アドレスの数と使用可能な IP アドレスの数を可視化する」ということをしました。
リファレンス通りにコマンドを打てばいいという訳ではなく、TerraformやHelmで管理しているレポジトリにアプローチをしなければならず、メンターの方にとてもサポートしていただいてタスクを進めていきました。
短い期間でしたが、TerraformでのIAM作成やArgoCDでのデプロイ、Grafanaでの可視化を一通りやることができて楽しかったです。
実務でKubernetesが動いている環境を触ったことが初めてで、アプリ開発の裏で何がどう動いているのかを確認できてよかったです。
高い技術を駆使して増え続けるユーザーを支えているチームには憧憬があります。
主なリファレンス
おわりに
働き方やタスクも柔軟に相談して決めることができたのでありがたかったです。
みてね の方々はもちろん、人事や労務の方々にも大変お世話になりました。
不満は特になく、あえて挙げるなら参加が決まってから現場に入るまでの連絡がSlackではなくて電話主体だったのでそわそわしたくらいです。
みてね に関しては普段過ごす上では聞いたことはなかったですが、海外展開も進んでいて1000万ダウンロード突破している温かみのあるアプリでした。
Slackにアプリの評価が流れてくるチャンネルがあり、そこを眺めるとモチベーションが上がるような環境でした。
そもそもミクシィという会社についてはmixiとモンストのイメージが強かったですが、選考に進むにつれてミクシルなどの記事を読む機会が増え、思ってたよりいいサービスが多いなーということに気づきました。
みてね だけに限ってもそれぞれの部署で強みのある方が多数おられて、技術的にもおもしろいなと思っています。
1ヶ月間ありがとうございました!
『Go言語で作るインタプリタ』をTypeScriptで実装する(後編)
前編を書いたのが約1年前なので1年間かかってしまった。
ずっとやってたわけではなく、前回は2章までできたらいいかって感じで終わらせて、1年越しに再開してみた。
Commits · wafuwafu13/Interpreter-made-in-TypeScript · GitHub
前回のブログの、「字句解析器(レキサー)」で述べている通り、基本的にはGoのStructをJavaScriptのclassに置き換えるとうまくいくけれど、詰まったところとかもあるので整理しておく。
まずは、新たに『Monkeyソースコードを評価する際に出てくる値全てをObjectで表現する』(p.121)ために作ったObjectインターフェースの差異からみていく。
これは前回と同様にクラスで構築していけばいい。
const( STRING_OBJ = "STRING" ) ... type String struct { Value string } func (s *String) Type() ObjectType { return STRING_OBJ } func (s *String) Inspect() string { return s.Value }
export const STRING_OBJ = 'STRING'; ... interface StringProps { value: string; } export class String<T extends StringProps> { value: T['value']; constructor(value: T['value']) { this.value = value; } inspect(): string { return this.value; } type(): ObjectType { return STRING_OBJ; } }
呼び出しの際、例えばevaluatorのevalStringInfixExpression
関数では、Goではこうなっているが、
return &object.String{Value: leftVal + rightVal}
今回はコンストラクタを返せばいい。
const evalStringInfixExpression = ( operator: string, left: any, right: any, ): any => { if (operator != '+') { return new Error( `unknown operator: ${left.type()} ${operator} ${right.type()}`, ); } const leftVal = left.value; const rightVal = right.value; return new String(leftVal + rightVal); };
evalatorのEval
関数では、Goの場合はType switchesを使ってast.Node
が保持している型によって処理を分けているが、
func Eval(node ast.Node, env *object.Environment) object.Object { switch node := node.(type) { case *ast.Program: return evalProgram(node, env) case *ast.ExpressionStatement: return Eval(node.Expression, env) ....
ast.ts
ではそもそもNodeインターフェースを定義していない。
今回の場合はnode.constructor.name
でクラス名を見てやることで解決する。
export const Eval = (node: any, env: Environment): any => { switch (node.constructor.name) { case 'Program': return evalProgram(node, env); case 'ExpressionStatement': return Eval(node.expression, env); ...
次に、evaluatorのevalInfixExpression
でoperatorが"=="
の際、Booleanオブジェクトを返す処理があった。
case operator == "==": return nativeBoolToBooleanObject(left == right)
TypeScriptの場合は、デバッグ結果が一見同じで、true判定して欲しいにも関わらず、false判定がなされてうまくいかなかった。
if (operator == '==') { console.log(left, right); // Boolean { value: false } Boolean { value: false } return new Boolean(left == right); }
Goの場合は、本にも書いてある通り、『ここで真偽値の等価性を確認するためにポインタの比較を使っている。これでうまくいくのは、私たちはオブジェクトを指し示すのに常にポインタを利用していて、かつ真偽値に関してはTRUEとFALSEの2つを使っているからだ...』(p.140)というようにポインタを使っているのでうまく判定される。
一方JavaScriptの場合は、異なるコンストラクタの同値判定はfalseになる。
class BooleanClass { constructor(value) { this.value = value } } const a = new BooleanClass(false); const b = new BooleanClass(false); console.log(a,b); // BooleanClass { value: false } BooleanClass { value: false } console.log(a == b); // false
なので、判定の際はvalue
までみにいって判定した。
if (operator == '==') { return new Boolean(left.value == right.value); }
p.171から、『このインタプリタはGoのガベージコレクタ(GC)を使っている』という記述があったが、今回も何も意識せずにJavaScriptのGCを使っていることになる。
メモリ管理 - JavaScript | MDN
4.4の配列の実装をしている途中で、何が原因かわからないバグにも遭遇した。
expressionのtokenに欲しいものが上位のtokenに入っててどこから壊れてるかがわからなくなってしまった pic.twitter.com/1RqJLWrk7E
— わふわふ (@wafuwafu13_) 2021年7月15日
結果的にはparserのparseIndexExpression
関数の第一引数にt: Token
を足したらなおった。
なおった理由もよくわかっていない。
最後に、p.207で配列の独自のビルトインとしてrestを構築する際、新しく割り当てられた配列を返さないといけなかった。
newElements := make([]object.Object, length-1, length-1) copy(newElements, arr.Elements[1:length]) return &object.Array{Elements: newElements}
Goの場合はcopyで配列のdeep copyはできるけれど、JavaScriptの場合は調べてみると結構やっかいだった。
What is the most efficient way to deep clone an object in JavaScript? - Stack Overflow
結果的にJSON.parse/JSON.stringify
を使えばうまくいった。
const newElements = JSON.parse(JSON.stringify(args[0].elements)).slice( 1, length, ); return new Array(newElements);
本にはテストコードがなかったけど、ちゃんとかいた。
it('testRest2', () => { const input = `let a = [1, 2, 3, 4]; rest(rest(rest(a)));`; const evaluated = testEval(input); expect(evaluated.elements.length).toBe(1); expect(evaluated.elements[0].value).toBe(4); });
という感じでGoとTypeScriptの差異に躓きながらも最後まで実装を進めていった。
本の最後のハッシュの章がまだバグがあって動かないから、また1年後にでもやるかもしれない。
1年前のコードを読みはじめたとき、よくこんな難解なコードを書いていたなと思ったから、コードを追う能力とかは1年前とさほど変わっていないと実感した。
今回やったのは、前回構築したParserを元に評価する、ということだから前回ほど大きな学びはなかったけど、いい復習になったし、言語の差異の壁を自力で乗り越えて本を1冊やり切ったことは自信にもなった。
ASTがいろんなところに使われていることも認識できるようになってきた。
graphql-validation-complexity から学ぶGraphQLのAST走査 - 経験は何よりも饒舌
が、まだGoやTypeScriptの仕様そのものがどう構築されているのかとか、GCとかまだよくわからないことの方が多いので後々詰めていけばいいと思っている。
Swift入門した
ReactNativeでアプリをリリースしたことはあるけど、Swiftは全く触ったことがなかった。
動機は夏に使いそうだからと、なんやかんやモバイル主体のプロダクトが多い気がするから。
Swift, Xcodeでの開発に慣れる
まずは手を動かしてみようと思って、Udemyのコースを一通りやった。
37時間もあってかなりボリューミーだった。
未経験者対象なので解説にうーむと思うところがあったけど、API連携とかJSON解析とかまで解説してあって、とりあえずSwift, Xcode, CocoaPods, UIKitでの開発の概要を掴めた。
Firebaseの章は全部飛ばした。
調べるとSwift UIっていうやつがあったのでこれもやってみた。
UIKitでの開発は固有の設定があって大変だったけどReact味があってよかった。
追記: mixiの研修が上がっていた(Swift UI)
www.youtube.com
Swiftの文法/用法を詳しく
文法/用法が曖昧だったところを調べていった。
どこよりも分かりやすいSwiftの"?"と"!" - Qiita
クロージャまとめ(Swift) - Qiita
GCDを使った非同期処理について改めて調べてみた | DevelopersIO
Swiftで複数の非同期処理の完了時に処理を行う - Qiita
Swiftらしいコーディングを学ぶ 「Generics」 - Qiita
使うと手放せなくなるSwift Extension集 (Swift 5版) - Qiita
Swiftの列挙型(enum)おさらい - Qiita
【Swift】 それ、enumとstructでやってみましょう!!
typealiasというSwiftの仕様を把握する - Qiita
Swiftにおける「文字」に関する型がとても多い件について - Qiita
Swiftのfuncの引数に出てくるアンダースコアやシャープの意味について調べた - Shoken Startup Blog
Swift の guard は正しく使いましょう - Qiita
Swiftにおけるプロトコル指向プログラミング
【Swift】delegate実装の流れ - Qiita
知っているようで知らないSwift5のアクセス修飾子 - Qiita
SwiftUIの機能 @State, @ObservedObject, @EnvironmentObjectの違いとは| 開発者ブログ | 株式会社アイソルート
Swiftのエラー4分類が素晴らしすぎるのでみんなに知ってほしい - Qiita
Heart of Swift | “Heart of Swift” は、 Swift の Heart である Value Semantics と Protocol-oriented Programming を軸に、 Swift という言語のコンセプトを説明するオンライン書籍です。
Heart of Swiftは
Swiftに限らずいい話だった
— わふわふ (@wafuwafu13_) 2021年8月1日
iOSの設計について
iOSの開発手法ついて調べているとこの本がおすすめされていたから読んだ。
10章で依存関係逆転の原則の説明があって初めて腑に落ちた。
Swiftによる依存関係逆転の原則 - Qiita
RxSwiftが出てきたから調べた。
RxSwiftについてようやく理解できてきたのでまとめることにした(1) - Qiita
はじめてのRxSwiftのメモ - Qiita
RxSwift 再入門 - Qiita
オブザーバーパターンから始めるRxSwift入門 - Qiita
[RxSwift] flatMapの呼び出しを恐がりたくない人生だった - Qiita
RxJSの概要を掴んだ方が理解が進んだ。
Reduxに関して自分が書いた記事も読み直した。
【定期更新】Redux公式チュートリアルのざっくりした和訳と流れ - Qiita
記事とレポジトリも読んだ。
iOSをMVC,MVP,MVVM,Clean Architectureで実装してみた | by ロクネム | Medium
iOSアプリの設計にClean Architectureを採用して約3年運用してきた知見 | by ロクネム | MIXI DEVELOPERS
Youtubeに良さげな動画があったのでやった。
github.com
github.com
インフラに関してやってきたこと/やりたいこと(2021年7月)
読んだ本
- キタミ式基本情報技術者
- WEB+DB 大規模サービス技術入門
- WEB+DB 24時間365日 サーバ/インフラを支える技術
- WEB+DB nginx実践入門
- Real World HTTP 歴史とコードに学ぶインターネットをウェブ技術
- Linuxで動かしながら学ぶ TCP/IP ネットワーク入門
- DNSがよくわかる教科書
- GCPの教科書
- Amazon Web Service 基礎からのネットワーク&サーバー構築
- Kubernetes on AWS
- Docker/Kubernetes実践コンテナ開発入門
- SREサイトリライアビリティエンジニアリング
- サイトリライアビリティワークブック
- Infrastructure as Code
- 入門監視
- クラウドインフラとAPIの仕組み
- Mackerelサーバ監視実践入門
- 実践Terraform
- ISUCONのススメ
経験
NTT Performance Tuning Contest
alpとかpt-query-digestとかの勉強をして対策をした。
wafuwafu13.hatenadiary.com
NginxやVarnish周りのコードを読んだり書いたりしているが、ssh権限はどのサーバーにもない。
この辺りの話は大体理解している。
developer.hatenastaff.com
障害対応を行なったことはないが、大規模サービスにおいてどのような障害が起こりうるのか、どのような対策をするのかは大体理解している。
Mackerelをどのように使用しているのかをなんとなく理解している。
Mackerelのソースコードを少し理解して簡単なPRを出したことがある。
wafuwafu13.hatenadiary.com
インターンでコンテナ等に関する講義を受けてGo/gRPC/k8sでブログを作った。
wafuwafu13.hatenadiary.com
hatenacorp.jp
AWA株式会社
wafuwafu13.hatenadiary.com
Terraformのバージョンアップをした。
Go/MongoDB/Mongo Connector/Elasticsearch/gRPC/ECS/Terraform/Ansible/Jenkins を触って検索処理の追加をした。
技術の概要を理解して使うことはできるが、長期間の運用をしたことはない。
graphql-validation-complexity から学ぶGraphQLのAST走査
オライリーの『初めてのGraphQL』の「7.3.4 クエリの複雑さ制限」で、graphql-validation-complexity というライブラリが紹介されていて、面白そうだったのでのぞいてみた。
graphql-validation-complexityは、クエリに対して複雑度を計算し、その複雑度に制限を設けることができる。
デフォルトでそれぞれのフィールドに値を設定し、リストが入れ子になるたびに値を10倍して、複雑度を計算している。
テストコードで例を示すと、以下のように複雑度が計算される。
query { item { name # 複雑度1 item { name # 複雑度10 } } list { item { name # 複雑度10 } list { name # 複雑度100 } } nonNullItem { name # 複雑度1 } nonNullList { name # 複雑度1 } } # 総複雑度123
この複雑度の計算の解釈が正しいことは、以下のテストが通ったことからも保証できる。
describe('simple queries', () => { it('should calculate the correct cost', () => { const cost = checkCost(` query { list { item { name } list { name } } } `); expect(cost).toBe(110); }); });
実際にどう使うかというと、例えばApolloを使う場合は、以下のようにvalidationRulesフィールドに設定する。
const apolloServer = new ApolloServer({ schema, validationRules: [createComplexityLimitRule(1000)], });
では、このcreateComplexityLimitRule
の中身を中身をみていく。
createComplexityLimitRule
関数は、src/index.js##L14-L17
でexportされており、引数に設定値をmaxCost
としてとっている。
graphql-validation-complexity/index.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub
export function createComplexityLimitRule( maxCost, { onCost, createError, formatErrorMessage, ...options } = {}, ) {
cost
が設定値のmaxCost
を超えていたら、エラーが吐かれるようになっている。
if (cost > maxCost) { context.reportError( createError ? createError(cost, node) : new GraphQLError(formatErrorMessage(cost), [node]), ); }
では、cost
はどのように求められているのかを調べると、以下のコードが目についた。
Document: { enter(node) { visit(node, visitWithTypeInfo(typeInfo, visitor)); }, leave(node) { const cost = visitor.getCost();
ここのvisit
関数はfrom 'graphql';
でインポートされているため、GraphQLのドキュメントを見てみると、以下のような説明があった。
visit() will walk through an AST using a depth first traversal, calling the visitor's enter function at each node in the traversal, and calling the leave function after visiting that node and all of its child nodes.
By returning different values from the enter and leave functions, the behavior of the visitor can be altered, including skipping over a sub-tree of the AST (by returning false), editing the AST by returning a value or null to remove the value, or to stop the whole traversal by returning BREAK.
When using visit() to edit an AST, the original AST will not be modified, and a new version of the AST with the changes applied will be returned from the visit function.
visit()は、深さ優先のトラバーサルを用いてASTを走査し、トラバーサル内の各ノードでビジターのenter関数を呼び出し、そのノードとその子ノードすべてを訪れた後にleave関数を呼び出します。
enter関数とleave関数から異なる値を返すことで、ビジターの動作を変更することができます。例えば、falseを返すことでASTのサブツリーをスキップしたり、値を返すことでASTを編集したり、nullを返すことで値を削除したり、BREAKを返すことでトラバーサル全体を停止したりすることができます。
visit()を使ってASTを編集する場合、元のASTは変更されず、変更が適用された新しいバージョンのASTがvisit関数から返されます。
このenter
にあるvisit()
によりAST走査がはじまるらしい。
テストコードを見てみると、const ast = parse(query);
で明示的にqueryがパースされていたので、enter(node)
のnode
はASTであることがわかる。
describe('ComplexityVisitor', () => { const typeInfo = new TypeInfo(schema); function checkCost(query) { const ast = parse(query); const context = new ValidationContext(schema, ast, typeInfo); const visitor = new ComplexityVisitor(context, {}); deepFreeze(ast); // ensure we aren't mutating accidentally visit(ast, visitWithTypeInfo(typeInfo, visitor)); return visitor.getCost(); } describe('simple queries', () => { it('should calculate the correct cost', () => { const cost = checkCost(` query { item { name item { name } ... `); expect(cost).toBe(123); }); });
ast
とvisitWithTypeInfo(typeInfo, visitor)
をデバッグすると以下のようであった。
console.log { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'query', name: undefined, variableDefinitions: [], directives: [], selectionSet: [Object], loc: [Object] } ], loc: { start: 0, end: 370 } } console.log { enter: [Function: enter], leave: [Function: leave] }
ast
をさらに詳しくデバッグすると、queryのフィールドはSelectionSet
で分割されていることがわかった。
console.log(ast.definitions[0].selectionSet);
console.log { kind: 'SelectionSet', selections: [ { kind: 'Field', alias: undefined, name: [Object], arguments: [], directives: [], selectionSet: [Object], loc: [Object] }, { kind: 'Field', alias: undefined, name: [Object], arguments: [], directives: [], selectionSet: [Object], loc: [Object] }, { kind: 'Field', alias: undefined, name: [Object], arguments: [], directives: [], selectionSet: [Object], loc: [Object] }, { kind: 'Field', alias: undefined, name: [Object], arguments: [], directives: [], selectionSet: [Object], loc: [Object] } ], loc: { start: 15, end: 363 } }
では、実際に複雑度を計算している箇所をみていく。
const cost = visitor.getCost();
のgetCost
関数はsrc/ComplexityVisitor.js#L221-L223
で定義されていて、
getCost() { return this.cost; }
119行目から127行目あたりで実際にcost
の計算がされていた。
enterField() { this.costFactor *= this.getFieldCostFactor(); this.cost += this.costFactor * this.getFieldCost(); } leaveField() { this.costFactor /= this.getFieldCostFactor(); }
ast
のselectionSet
がいじられている箇所があったことから、selectionSet
を使って複雑度に重みを与えていることが想像できる。
this.SelectionSet = this.flattenFragmentSpreads; } flattenFragmentSpreads(selectionSet) { const nextSelections = selectionSet.selections.flatMap((node) => { ....
複雑度が計算されて値に応じてエラーが返る流れはなんとなく掴めた。
が、graphql-validation-complexityとGraphqlのvisit()がどのように噛み合ってるのかがいまいちよくわからない。
ComplexityVisitor
クラスのコンストラクタにあるthis.Field
のenter/leave
が発火しているっぽいが、そもそもthis.Field
が何を指しているのかわかっていない。
this.Field = { enter: this.enterField, leave: this.leaveField, };
さっきみたように、visitWithTypeInfo
によってenter/leave
が作られているから、visitWithTypeInfo
を調べればわかるかもしれないが、ドキュメントを調べても出てこないから諦める。
const visitor = new ComplexityVisitor(context, options); const typeInfo = context._typeInfo || new TypeInfo(context.getSchema()); console.log(visitWithTypeInfo(typeInfo, visitor)) // { enter: [Function: enter], leave: [Function: leave] }
にしてもいろんなところでASTが使われている。
Go の main goroutine が exit する場所
ascii.jp
を進めているメモ。
package main import "fmt" func main() { fmt.Println("Hello World!") }
go/src/runtime/proc.go#L277
go/proc.go at 912f0750472dd4f674b69ca1616bfaf377af1805 · golang/go · GitHub
exit(0)
あと、連載の時からwrite()
メソッドが書き換わっていた。
ここから先は、環境によって固有のコードに飛びます。 Unix系OS(macOSやLinuxなど)では、write() メソッドは次のようになっていると思います。 for ループに囲まれて、送信が終わるまで何度も syscall.Write を呼んでいることがわかります。 この syscall.Write がシステムコールです。
func (f *File) write(b []byte) (n int, err error) {
for {
bcap := b
if needsMaxRW && len(bcap) > maxRW {
bcap = bcap[:maxRW]
}
m, err := fixCount(syscall.Write(f.fd, bcap))
n += m
:
}
}
実際には以下のようになっていた。
go/file_posix.go at 912f0750472dd4f674b69ca1616bfaf377af1805 · golang/go · GitHub
// write writes len(b) bytes to the File. // It returns the number of bytes written and an error, if any. func (f *File) write(b []byte) (n int, err error) { n, err = f.pfd.Write(b) runtime.KeepAlive(f) return n, err }
f.pfd.Write
はfd_unix.go
で呼び出されているので、「環境によって固有のコードに飛びます」はこのことを言ってそう。
syscall.Write
もここで呼び出されている。
go/fd_unix.go at 912f0750472dd4f674b69ca1616bfaf377af1805 · golang/go · GitHub
// Write implements io.Writer. func (fd *FD) Write(p []byte) (int, error) { if err := fd.writeLock(); err != nil { return 0, err } defer fd.writeUnlock() if err := fd.pd.prepareWrite(fd.isFile); err != nil { return 0, err } var nn int for { max := len(p) if fd.IsStream && max-nn > maxRW { max = nn + maxRW } n, err := ignoringEINTR(func() (int, error) { return syscall.Write(fd.Sysfd, p[nn:max]) }) if n > 0 { nn += n } if nn == len(p) { return nn, err } if err == syscall.EAGAIN && fd.pd.pollable() { if err = fd.pd.waitWrite(fd.isFile); err == nil { continue } } if err != nil { return nn, err } if n == 0 { return nn, io.ErrUnexpectedEOF } } }
大体以下のような流れでfmt.Println("Hello World!")
がされていることがわかった。
func Println(a ...interface{}) (n int, err error)
=>
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)
=>
func (f *File) Write(b []byte) (n int, err error)
=>
func (f *File) write(b []byte) (n int, err error)
=>
func (fd *FD) Write(p []byte) (int, error)
=>
syscall.Write(fd.Sysfd, p[nn:max])
=>
exit(0)
Goのコードはよくわからないからこんな感じで外観を掴むだけでよさそう。
go/src/runtime/proc.go
のコメントを和訳してみたけどよくわからない。
ゴルーチン・スケジューラ
スケジューラの仕事は、すぐに実行可能なgoroutineをワーカースレッドに分配することです。主な概念は次のとおりです。
G - ゴルーチン。
M - ワーカースレッド、またはマシン。
P - プロセッサ。Goコードを実行するために必要なリソースです。
Mは、Goコードを実行するために、関連するPを持っていなければなりません。
ブロックされたり、関連付けられたPを持たないシステムコールの中にいたりします。https://golang.org/s/go11sched のデザインドキュメントをご覧ください。
ワーカースレッドのパーキング/アンパーキング。
利用可能なハードウェアの並列性を利用するために十分な数のワーカースレッドを走らせておくことと、ハードウェアの並列性を利用するために十分な数のワーカースレッドを確保することとを駐留させて、CPUリソースと電力を節約することが必要です。
これは2つの理由から簡単ではありません。
(1) スケジューラの状態は(特に、P単位の作業キュー)が意図的に分散されているため、高速な経路でグローバルな述語を計算することはできません。
(2) 最適なスレッド管理のためには、未来を知る必要がある(近い将来、新しいゴルーチンが準備されるときにワーカスレッドをパークしない)。悪い方向に働くであろう3つの拒絶されたアプローチ
1. スケジューラの状態をすべて集中管理する(スケーラビリティが阻害される)。
2. ゴルーチンの直接ハンドオフ。つまり、新しいゴルーチンの準備ができて、予備のPがあるときにスレッドをアンパークし、スレッドとゴルーチンをハンドオフします。
これでは、ゴルーチンを準備したスレッドがスレッドの状態を崩してしまうことになります。
ゴルーチンを準備したスレッドは、次の瞬間には仕事をしていない可能性があるので、それをパークする必要があります。
また、計算の局所性も失われてしまいます。
また、同一スレッド上に依存するゴルーチンを保持したいため、計算の局所性が失われ、さらなるレイテンシーが発生します。
3. ゴルーチンの準備ができてアイドルPがあるときは、追加のスレッドをアンパークします。
アイドル状態のPがあるときに追加のスレッドをアンパークするが、ハンドオフは行わない。これは、追加スレッドのパーキング/アンパーキングが過剰になります。
スレッドを過剰にパーキング/アンパーキングすることになります。
する作業を発見することなく、即座にパークしてしまうからです。現在のアプローチは(1) アイドル状態の P があり、「回転している」ワーカースレッドがない場合、ゴルーチンの準備時に追加のスレッドをアンパークします。
アイドルPが存在し、「回転している」ワーカースレッドが存在しない場合。ワーカースレッドが回転していると考えられるのは
紡いでいるとみなされるのは、ローカルな仕事がなく、グローバルなランキュー/ネットポラーで仕事を見つけられなかった場合です。
スピニング状態は m.spinning および sched.nmspinning で示されます。
このようにしてパークされていないスレッドもスピンしているとみなされますが、私たちはゴルーチンハンドオフを行わないので、そのようなスレッドは最初は仕事がありません。
スピンするスレッドは、パーキングする前に、Pごとの実行キューで仕事を探すためにいくつかのスピンを行います。
もしスピニングスレッドは仕事を見つけると、自分自身をスピニング状態から解放し、実行に進みます。
仕事を見つけられなかった場合は、スピニング状態から抜け出してパークします。
少なくとも1つの回転中のスレッドがある場合(sched.nmspinning>1)、ゴルーチンの準備中に新しいスレッドをアンパークすることはありません。
ゴルーチンを準備する際に新しいスレッドをアンパークしません。
これを補うために、最後に回転していたスレッドが仕事を見つけて回転を止めた場合は新しい回転中のスレッドをアンパークしなければなりません。
この方法により、スレッドのアンパークが不当に急増することがなくなります。主な実装上の問題点は、スピンしているスレッドとスピンしていないスレッドの間の紡績->非紡績のスレッド遷移の際に非常に注意する必要があることです。
この移行は、新しいゴルーチンの投入と競合する可能性があります。
両方がアンパークに失敗すると、半永久的にCPUの使用率が低下することになります。
ゴルーチンの準備の一般的なパターンは、ゴルーチンをローカルワークキューに投入し、#StoreLoadスタイルのメモリバリアを行い、sched.nmspinningをチェックします。
spinning->non-spinningの遷移の一般的なパターンは、nmspinningをデクリメントすることです。
Nmspinningをデクリメントし、#StoreLoad形式のメモリバリアで、すべてのper-Pのワークキューに新しい仕事がないかチェックする。
これらの複雑さは、グローバルランキューには適用されないことに注意してください。
グローバルキューへの投入時にスレッドのアンパークを怠らないからです。また、コメントを参照してください。