経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

非推奨になった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.jsdocs/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.mdThis 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チームという形で動きました。

mitene.us

アプリ開発チーム

アプリ開発チームでは複数のプロジェクトが同時進行で動いていて、それぞれのチームでiOS, Android, Railsの開発を進めていくという体制が取られていました。
自分はその中のひとつのプロジェクトに入って、そのプロジェクトの機能を少し拡張するために、モデルの関連の追加、管理画面の作成、今後の方針を立てる、ということをしました。
時間あたりの作業量は普段より少なく、難易度も至って普通でしたが、単独したタスクや既存の改善ではなく、新しくプロジェクトが進んでいる段階でモデルの作成から入る経験は少なかったため、開発のフローやスピード感がリアルに感じれました。
メンターの方が2年目だったため、仕事のイメージも湧きやすく、直接話も聞けたのでよかったです。
モバイルの画面を作ったりすることはなかったですが、リファクタリングのタスクが落ちていたので、方針に沿ってリファクタ作業もしました。
Swiftを実務で使うのは初めてで、設計まで踏み込んで読めたのはよかったです。
モバイルからサーバーまでやっているチームと聞いて、主にモバイルをやってサーバーはちょっとした変更に留まっているようなイメージを持っていましたが、DB設計からモバイルのアーキテクチャ設計まで思っていたよりガッツリやられていました。
1ヶ月という期間もあり、自分は0=>1のDB設計の段階の少しに関わったり、あらかじめ方針が固まったリファクタをしたに過ぎないですが、普段から両方に目をやれる体制が自分には合っているなと感じました。
チーム内でiOS, Android, Railsの足並みを揃えるのもスムーズにやられているように感じたのと、デザインに関しては みてね のチーム全体でより良いものに改善していく取り組みがなされていたので、そこの体制も良いなと思いました。
あと単純にどんどん新しい施策が0から生まれていってる環境は楽しいなと感じました。

主なリファレンス

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年間かかってしまった。

wafuwafu13.hatenadiary.com

github.com


ずっとやってたわけではなく、前回は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)を使っている』という記述があったが、今回も何も意識せずにJavaScriptGCを使っていることになる。
メモリ管理 - JavaScript | MDN

4.4の配列の実装をしている途中で、何が原因かわからないバグにも遭遇した。

結果的には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のコースを一通りやった。

www.udemy.com
github.com

37時間もあってかなりボリューミーだった。
未経験者対象なので解説にうーむと思うところがあったけど、API連携とかJSON解析とかまで解説してあって、とりあえずSwift, Xcode, CocoaPods, UIKitでの開発の概要を掴めた。
Firebaseの章は全部飛ばした。

www.udemy.com
github.com

調べると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は

テスト

ユニットテストとUIテストに関する動画があったのでやってみた。
github.com

www.youtube.com
www.youtube.com

デザイン

iOS開発の場合特に気をつけないといけない雰囲気があったから本を一冊読んだ。

デザインの本は全部読んだら分かるで終始してしまいそうで難しい。

感想

Swiftの言語仕様もそこまで難しくない?し、趣味でアプリ開発をしたこともあるし、設計もWebアプリケーションと被る部分はあるので、まだ深くやってないけど技術的な障壁はそこまでないと思う。
ただ、XCodeとかUIKit使ってやるっていう独自性があったり、個人的にiOSiOSの専門職人がいるイメージが強いから精神的な障壁はあると思う(Android勢ならなおさら)。

インフラに関してやってきたこと/やりたいこと(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 を触って検索処理の追加をした。
技術の概要を理解して使うことはできるが、長期間の運用をしたことはない。

やりたいこと

  • とにかくssh接続できたらなんでもやりたい
  • AWS/GCP/k8s/Docker/Terraform あたりの技術を本番環境で長期的に触れてみたい
  • アプリケーションから見えるインフラ、見えないインフラの両方に携わりたい

graphql-validation-complexity から学ぶGraphQLのAST走査

オライリー『初めてのGraphQL』の「7.3.4 クエリの複雑さ制限」で、graphql-validation-complexity というライブラリが紹介されていて、面白そうだったのでのぞいてみた。

github.com

graphql-validation-complexityは、クエリに対して複雑度を計算し、その複雑度に制限を設けることができる。
デフォルトでそれぞれのフィールドに値を設定し、リストが入れ子になるたびに値を10倍して、複雑度を計算している。
テストコードで例を示すと、以下のように複雑度が計算される。

graphql-validation-complexity/ComplexityVisitor.test.js at master · 4Catalyzer/graphql-validation-complexity · GitHub

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を超えていたら、エラーが吐かれるようになっている。

graphql-validation-complexity/index.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub

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のドキュメントを見てみると、以下のような説明があった。

graphql.org

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であることがわかる。

graphql-validation-complexity/ComplexityVisitor.test.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub

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);
    });
  });

astvisitWithTypeInfo(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で定義されていて、

graphql-validation-complexity/ComplexityVisitor.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub

  getCost() {
    return this.cost;
  }

119行目から127行目あたりで実際にcostの計算がされていた。

graphql-validation-complexity/ComplexityVisitor.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub

 enterField() {
    this.costFactor *= this.getFieldCostFactor();

    this.cost += this.costFactor * this.getFieldCost();
  }

  leaveField() {
    this.costFactor /= this.getFieldCostFactor();
  }

astselectionSetがいじられている箇所があったことから、selectionSetを使って複雑度に重みを与えていることが想像できる。

graphql-validation-complexity/ComplexityVisitor.js at 1be812179e5da7d850b76441b135ed178ce769f5 · 4Catalyzer/graphql-validation-complexity · GitHub

    this.SelectionSet = this.flattenFragmentSpreads;
  }

  flattenFragmentSpreads(selectionSet) {
    const nextSelections = selectionSet.selections.flatMap((node) => {
  ....

複雑度が計算されて値に応じてエラーが返る流れはなんとなく掴めた。
が、graphql-validation-complexityとGraphqlのvisit()がどのように噛み合ってるのかがいまいちよくわからない。
ComplexityVisitorクラスのコンストラクタにあるthis.Fieldenter/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!")
}

VSCodeデバッグして最後にexitするのはここ。

go/src/runtime/proc.go#L277
go/proc.go at 912f0750472dd4f674b69ca1616bfaf377af1805 · golang/go · GitHub

exit(0)


あと、連載の時からwrite()メソッドが書き換わっていた。

ここから先は、環境によって固有のコードに飛びます。 Unix系OSmacOSLinuxなど)では、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.Writefd_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のワークキューに新しい仕事がないかチェックする。
これらの複雑さは、グローバルランキューには適用されないことに注意してください。
グローバルキューへの投入時にスレッドのアンパークを怠らないからです。また、コメントを参照してください。