Resource Timing APIについて(簡単な検証を添えて)

Web APIはたくさんあるが、その中のPerformance APIの一部である Resource Timing APIについて書きたいと思う。

Resource Timing APIとは

サイトのページのリソース(ex: 画像とかcssファイルとか)を読み込む際のネットワークタイミングの詳細情報を取得することができるAPI

サイトのページのレスポンス速度はユーザー体験にも影響を与えるため大事だが、このAPIは個々のリソースが読み込まれるまでにどれくらい時間を要しているのか、読み込まれるまでにステップのどこに時間がかかっているのかを知るための情報を提供してくれる。

リソースを読み込む際の各地点それぞれのtimestampを計測していて、APIを利用する側はResource Timing APIで定義されたプロパティにアクセスすればtimestamp値を取得することができる。

MDNのドキュメントには、リソースを読み込む全体のフロー図とともにどの地点のtimestamp値を取得し、提供しているのかがが記載されている。

※MDNのドキュメントを引用

取得できる値は次のものがある。(MDNから引用した)

  • startTime: リソース読み込み処理を開始した直前のタイムスタンプ。
  • redirectStart: リダイレクトを開始したフェッチのタイムスタンプ。
  • redirectEnd: 最後のリダイレクトに対するレスポンスの最後のバイトを受信した直後のタイムスタンプ。
  • workerStart: サービスワーカーのスレッドを起動した直前のタイムスタンプ。
  • fetchStart: ブラウザーがリソースの取得を始める直前のタイムスタンプ。
  • domainLookupStart: ブラウザーがリソースのドメイン名検索を始める直前のタイムスタンプ。
  • domainLookupEnd: ブラウザーがリソースのドメイン名検索を完了した直後のタイムスタンプ。
  • connectStart: ユーザーエージェントがリソースを取得するためにサーバーへの接続を確立し始める直前のタイムスタンプ。
  • secureConnectionStart: リソースが安全な接続で読み込まれた場合、ブラウザーが現在の接続を安全にするためにハンドシェイクプロセスを開始した直前のタイムスタンプ。
  • connectEnd: ブラウザーがサーバーへの接続を完了してリソースを取得した直後のタイムスタンプ。
  • requestStart: ブラウザーがサーバー、キャッシュ、ローカルリソースからリソースをリクエストし始める直前の時点のタイムスタンプ。
  • responseStart: ブラウザーがサーバー、キャッシュ、ローカルリソースからレスポンスの最初のバイトを受信した直後のタイムスタンプ。
  • responseEnd: ブラウザーがリソースの最後のバイトを受信した直後、またはトランスポート接続が閉じられた直前にタイムスタンプが記録されます。どちらが最初のイベントになるかは決まっていません。

これらのプロパティは DOMHighResTimeStamp という型が定義されている。

取得した各timestamp値を活用することでどのステップにどれくらい時間がかかったのかを計算して確認することができる。

  • TCP ハンドシェイク時間の測定 ( connectEnd - connectStart )
  • DNS ルックアップ時間の測定 ( domainLookupEnd - domainLookupStart )
  • リダイレクト時間の測定 ( redirectEnd - redirectStart )
  • リクエスト時間の測定 ( responseStart - requestStart )
  • TLS ネゴシエーション時間の測定 ( requestStart - secureConnectionStart )
  • フェッチに要する時間(リダイレクトなし)の測定 ( responseEnd - fetchStart )
  • サービスワーカーの処理時間の測定 (fetchStart - workerStart)

これらのtimestamp値のほかにも取得したリソースのサイズ情報も提供している。(MDNから引用した)

  • transferSize : 取得したリソースのサイズをバイト単位で返す。(レスポンスヘッダーフィールドやレスポンス内容本体がサイズに含まれる)
  • encodedBodySize : 適用されたコンテンツエンコーディングが除去される前の、HTTPまたはキャッシュから取得した内容本体のサイズをオクテット単位で返す
  • decodedBodySize : 適用されたコンテンツエンコーディングが除去された後の、HTTPまたはキャッシュから取得したメッセージ本体のサイズをオクテット単位で返す

ネットワークの通信によってリソースが取得された場合、transferSizeは0より大きい値を出力する。ブラウザなどのローカルキャッシュがヒットしていたらtransferSizeは0になる。encodedBodySizeやdecodedBodySizeはキャッシュからリソースを取得してもネットワークの通信によってリソースを取得しても0より大きい値を出力する。

encodedBodySizeとdecodedBodySizeの比較によって読み取ったリソースが圧縮されているのかを確認することもできる。decodedBodySizeとencodedBodySizeが同じ値でないとき圧縮していると判断することができる。

Resouce Timing APIとTiming Allow Origin header

Resource Timing APIを利用するとき、オリジン以外のリソースのネットワークタイミングの情報を知るにはTiming Allow Origin ヘッダーを利用する必要がある。

オリジン以外のリソースのネットワークタイミングの情報を読み取ろうすると先ほど記載したプロパティのほとんどが0として出力される。(そういう仕様になっている)

もしオリジン以外のリソースのネットワークタイミングの情報を取得したい場合、Timing Allow Origin ヘッダーを利用してResource Timing APIが提供する属性値を読み取りの許可をする必要がある。

例えば test.com というオリジンにおいて、test2.comのリソース情報を取得したい場合、リソース側のレスポンスヘッダーにTiming Allow Originをセットする必要がある

Timing-Allow-Origin: https://test.com

## もしくはワイルドカードによってどのオリジンでも許可
Timing-Allow-Origin: *

これによって test.com オリジンで動作するJavaScriptでリソースの情報を取得することができる。

検証

検証環境

実際にResource Timing APIから出力される値がどのように変わるのかをいくつか条件を変えて検証してみた。

利用ブラウザ : Chrome, Safari

リソースはcloudflare R2に画像を配置し、cloudflare workerでTiming-Allow-Originヘッダーを付与して画像リソースを返すようにしている。

export default {
    async fetch(request, env, ctx): Promise<Response> {
        if (request.method !== 'GET') {
            return new Response('Method Not Allowed', {
                status: 405,
                headers: {
                    'Allow': 'GET',
                },
            });
        }

        const url = new URL(request.url);
        const key = url.pathname.slice(1);

        if (!key) {
            return new Response('Not Found', { status: 404 });
        }

        try {
            const object = await env.R2.get(key);

            if (object === null) {
                return new Response('Not Found', { status: 404 });
            }

            // レスポンスヘッダーを設定
            const headers = new Headers();
            object.writeHttpMetadata(headers);
            headers.set('etag', object.httpEtag);
            headers.set('cache-control', 'public, max-age=900');
            headers.set('timing-allow-origin', 'https://rta-test.yokishava.com');

            return new Response(object.body, {
                headers,
            });
        } catch (error) {
            return new Response('Internal Server Error', { status: 500 });
        }
    },
} satisfies ExportedHandler<Env>;

クロスオリジンでR2の画像にアクセスし、Resource Timing APIを呼び出してそれぞれの値を出力するページを用意した。こちらはcloudflare pagesにdeployした。

<!doctype html>
<html lang="en">

<head>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>Resource Timing API 検証</title>
</head>

<body>
    <h1>Resource Timing API 検証</h1>
    <img id="test-img-1" src="https://r2-worker.xxxxxx.workers.dev/test1.png" />
    <div id="results"></div>
    <script>
       window.addEventListener('load', () => {
            setTimeout(measurePerformance, 100);
        });

       function measurePerformance() {
            const imageUrl = 'https://r2-worker.xxxxxx.workers.dev/test1.png';
            const entries = performance.getEntriesByName(imageUrl);

            if (entries.length > 0) {
                const timing = entries[0];
                const results = {
                    'fetchStart': timing.fetchStart,
                    'responseEnd': timing.responseEnd,
                    'duration': timing.duration,

                    'redirectStart': timing.redirectStart,
                    'redirectEnd': timing.redirectEnd,
                    'domainLookupStart': timing.domainLookupStart,
                    'domainLookupEnd': timing.domainLookupEnd,
                    'connectStart': timing.connectStart,
                    'connectEnd': timing.connectEnd,
                    'secureConnectionStart': timing.secureConnectionStart,
                    'requestStart': timing.requestStart,
                    'responseStart': timing.responseStart,

                    'transferSize': timing.transferSize,
                    'encodedBodySize': timing.encodedBodySize,
                    'decodedBodySize': timing.decodedBodySize,

                    'dnsTime': timing.domainLookupEnd - timing.domainLookupStart,
                    'tcpTime': timing.connectEnd - timing.connectStart,
                    'sslTime': timing.secureConnectionStart > 0 ?
                        timing.connectEnd - timing.secureConnectionStart : 0,
                    'ttfb': timing.responseStart - timing.requestStart,
                    'downloadTime': timing.responseEnd - timing.responseStart
                };
                displayResults(results);
            }
        }

       function displayResults(results) {
            const div = document.getElementById('results');
            div.innerHTML = '<h2>測定結果</h2><pre>' +
                JSON.stringify(results, null, 2) + '</pre>';
        }
   </script>
</body>
</html>

Chrome

Timing Allow Origin ヘッダーなしで初回アクセスを想定

ヘッダーなしの場合だと、domainLookupStartなどにアクセスしても0が返されていた。

※ラーメンの画像がクロスオリジンで取得したリソースである。

Timing Allow Origin ヘッダーなしで2回目以降のアクセスを想定(ブラウザキャッシュ有効でcache-controlヘッダーに設定した900秒以内にアクセス)

Timing Allow Origin ヘッダーが付与されていないのでまだ情報を取得することができないが、リソースはメモリキャッシュから取得されたので、fetch開始からレスポンス終了までにどれくらいの時間が経過したのかを示すdurationは以前よりも短くなっている。

Timing Allow Origin ヘッダーありで初回アクセスを想定

リソースの情報を取得することができた。今回の実装で取得するリソースはリダイレクトが発生するものではないので、redirectStart, redirectEndはそれぞれ0になっている。DNS解決からTCP, TLSの接続もstartとendでそれぞれ違うことがわかる。(wiresharkdnsだけ追っていたが、ログが取得されたのでDNSのキャッシュではなく問い合わせが行われていたことがわかる)

ネットワークの通信によってリソースが取得されて、圧縮も行っていないので transferSize, encodedBodySize, decodedBodySizeも同じ値になっていることがわかる。

Timing Allow Origin ヘッダーありで2回目以降のアクセスを想定(ブラウザキャッシュ有効でcache-controlヘッダーに設定した900秒以内にアクセス)

メモリキャッシュが効いたので、transferSizeが0になっている。(encodedBodySizeとdecodedBodySizeは0ではない。)

domainLookupStartからresponseEndまでがすべて fetchStartと同じ値になっている。

同じ値になっている理由は次のとおりである。 fetch処理が開始されるときにブラウザがfetchStartのtimestamp値をセットする。同時にdomainLookupStart, domainLookupEnd, connectionStart, connectionEndにもfetchStartにセットした値をセットする。つまりこの時点でfetchStartと同じ値がセットされていることになる。メモリキャッシュによってリソースを再利用する場合、domainLookupStartに値をセットするような後続のステップが中止されるので結果的にfetchStartの値と同じ値がdomainLookupStartなどの出力値として表示された。

それぞれのtimestamp値が決定されるステップはW3C5.1のProcessing Modelのセクションに記載されていて、上記のステップは全体のステップの5, 6に該当するものである。

ちなみに secureConnectionStartは下記のように定義されている。

The same value as fetchStart, if a persistent connection [RFC7230] is used or the resource is retrieved from relevant application caches or local resources.

※[4.3 The PerformanceResourceTiming Interface]のsecureConnectionStartのセクションの文章を引用

キャッシュなどからリソースが取得される場合はfetchStartと同じ値になる。

Timing Allow Origin ヘッダーありで2回目以降のアクセスを想定(ブラウザキャッシュ無効)

ブラウザのキャッシュを無効にしたので、transferSize, encodedBodySize, decodedBodySizeの値が0より大きい値で記録されている。DNS問い合わせはdomainLookupStart, domainLookupEndが同じ値になっているので、DNS問い合わせはなくDNSのキャッシュが効いてると見て良さそう。(Wiresharkの方にもパケット情報は見られなかった)一方でリソースはネットワークの通信によってリソースを取得してきているので、connectionEndとdomainLookupEndが同値であることを除いて、connectionStart, connectionEnd, secureConnectionStartの値がそれぞれ異なり、domainLookupStart, domainLookupEndとも値が異なっているのがわかる。

Safari

Timing Allow Origin ヘッダーありで初回アクセスを想定

safariでも同様にリソースのtimestamp情報を取得することができた。しかし、リソースのサイズ情報がすべて0になっている。Timing Allow Originヘッダーが設定されていなかったり、設定の情報が間違っているならばtimestamp情報も0になるはずだが、timestamp情報はきちんと取得することができている。

この理由について調べてみたが、おそらくW3Cに記載されているある文章に該当するのではないかと考えている。(間違っていたら誰か教えてほしい)

The user agent MAY still enforce cross-origin restrictions and set transferSize, encodedBodySize, and decodedBodySize attributes to zero.

※ (4.5.1 Timing-Allow-Origin Response Header)https://www.w3.org/TR/resource-timing/の文章を引用

この文章のユーザーエージェントはブラウザのことを指していたりすると解釈している。(きちんとヘッダーが付与されていても)size情報に0を適用してもいいと記載されている。Resource Timing APIを利用する側のために、Resource Timing APIで定義されている値を計測してセットするのはブラウザ側が行っているはずなので、Safariがあえてこのようにしているのかもしれない。

Timing Allow Origin ヘッダーありで2回目以降のアクセスを想定(ブラウザキャッシュ有効でcache-controlヘッダーに設定した900秒以内にアクセス)

ブラウザのメモリキャッシュが効いているので、size情報を除いてChromeのときと同様の結果となった。

Timing Allow Origin ヘッダーありで2回目以降のアクセスを想定(ブラウザキャッシュ無効)

リソース自体にキャッシュが効いてないことは確認できたがconnectionStart, connectionEndが同値になっている。リソースを取得するためのtcp接続があったはずなのでChromeの時と同様に異なる値になっていてもいい気がしているのだが...(このポストを書いてるときに気づいた。数字も丸められているのも気になってきた...)

Chromeと比較するとsize情報だけでなく、timestamp情報もキャッシュのあるなしに関係なく初回のアクセスのみでしか精緻な値を取得することが難しそうだった。

さいごに

ChromeSafariでもパフォーマンスAPI及びResource Timing APIをサポートしているが、概ねドキュメントに書いてある仕様通りに値を取得することができた。しかし、実際に動かしてみたらブラウザごとに少しずつ挙動が違ったりした(特にSafari)。今後活用する機会があったときに今回の検証が役立つと嬉しい。

参考

リソースタイミング - Web API | MDN

Timing-Allow-Origin - HTTP | MDN

Resource Timing Level 1

High Resolution Time Level 2

HTML Standard

コンピュータシステムの理論と実装の6章をやり直した

前回からの続き。アセンブラを実装した。 Rustで書いた。

実装したモジュール群

  • symble table : ラベルなどのシンボルとメモリアドレスを対応させるためのテーブル。
  • parser : アセンブリ言語で定義された各コマンドのパーサー。
  • code : ニーモニックをバイナリに変換するためのモジュール。

全体の処理の流れ

合計2回 .asm ファイルの内容を読み取るように実装している。

1回目の各行の読み取りでは、ラベルとROMアドレスをマッピングするための処理を行っている。

ラベル自体を機械語に変換せずに、ラベルの次の命令のROMアドレスと対応付けを行う。

  @i
  M=1
  @sum
  M=0
(LOOP)
  @i
  ...(省略)
  @LOOP
  0;JMP
  ...(つづく

例えば上記のようなアセンブリがあった場合、 LOOP , 0;JMP の後に (LOOP) の次の命令を実行できるようにしたい。

この命令の流れを実現するために、ROMアドレスの初期値を0から始め、アセンブリを1行ずつ見ていきA命令やC命令の場合はROMアドレスをインクリメントしていく。 (LOOP) のラベルが出現したときに、 LOOP と次の命令(最初の命令から数えると4番目の命令になる)のROMアドレスをシンボルテーブルに登録する。(上記の例だと、LOOP : 4 という対応がシンボルテーブルに登録される)

これらの処理を1回目の読み取りの際に行う。

2回目の読み取りで、A命令やC命令をパースして機械語を生成していく。

1つずつ命令を読み取っていき、どの命令に分類されるかを判別していく。

@ から始まっていたらA命令になり、 ラベルを除いた残りの命令はC命令になる。

A命令の場合、 @ 以降の文字列もしくは数字を取り出して機械語に変換する。

  @i
  @10

どちらの処理もAレジスタに格納されるが、 i の場合はRAMのアドレスに変換する必要がある。 すでにシンボルテーブルに i に対応するアドレスがある場合はそれを返すようにする。 i に対応するRAMアドレスが存在しない場合は i とRAMアドレスの対応をシンボルテーブルに新たに登録する。 RAMアドレスの0~15は事前に定義されているので、16を初期値としてアセンブラの実行を始める。シンボルテーブルにRAMアドレスが登録されていくたびにRAMアドレスをインクリメントしていく。  @10 のようなA命令が数字で定義されている場合は、定義されている数字をそのまま機械語として返す。

C命令だった場合、C命令の基本的な構造である dest=comp;jump に沿ってパースしていく。

命令によって =; があったりなかったりするので、それを考慮してdest, comp, jumpそれぞれのニーモニックを抽出するようにパーサーを実装する。

書籍のAPIのガイドに沿ってRustで実装したものを貼っていく。

symbol table

#[derive(Debug, Clone)]
pub struct SymbolTable {
    table: HashMap<String, u16>,
}

impl SymbolTable {
    pub fn new() -> Self {
        Self {
            table: HashMap::from([
                ("SP".to_string(), 0),
                ("LCL".to_string(), 1),
                ("ARG".to_string(), 2),
                ("THIS".to_string(), 3),
                ("THAT".to_string(), 4),
                ("R0".to_string(), 0),
                ("R1".to_string(), 1),
                ("R2".to_string(), 2),
                ("R3".to_string(), 3),
                ("R4".to_string(), 4),
                ("R5".to_string(), 5),
                ("R6".to_string(), 6),
                ("R7".to_string(), 7),
                ("R8".to_string(), 8),
                ("R9".to_string(), 9),
                ("R10".to_string(), 10),
                ("R11".to_string(), 11),
                ("R12".to_string(), 12),
                ("R13".to_string(), 13),
                ("R14".to_string(), 14),
                ("R15".to_string(), 15),
                ("SCREEN".to_string(), 16384),
                ("KEYBOARD".to_string(), 24576),
            ]),
        }
    }
    pub fn add_entry(&mut self, symbol: String, address: u16) {
        self.table.insert(symbol, address);
    }

    pub fn contains(&self, symbol: &str) -> bool {
        return self.table.contains_key(symbol);
    }

    pub fn get_address(&self, symbol: &str) -> Option<&u16> {
        return self.table.get(symbol);
    }
}

parser

#[derive(Debug, Clone)]
pub struct Parser<'a> {
    command: &'a str,
}

#[derive(Debug, Clone)]
pub enum CommandType {
    ACommand,
    CCommand,
    LCommand,
}

impl<'a> Parser<'a> {
    pub fn new(command: &'a str) -> Self {
        Self { command: command }
    }

    pub fn command_type(&self) -> CommandType {
        if self.command.starts_with("@") {
            return CommandType::ACommand;
        }

        if self.command.starts_with("(") && self.command.ends_with(")") {
            return CommandType::LCommand;
        }

        return CommandType::CCommand;
    }

    pub fn symbol(&self) -> String {
        return self
            .command
            .replace("@", "")
            .replace("(", "")
            .replace(")", "");
    }

    pub fn dest(&self) -> Option<String> {
        if self.command.contains("=") {
            let keywords: Vec<&str> = self.command.split("=").collect();
            return Some(keywords[0].to_string());
        }
        return None;
    }

    pub fn comp(&self) -> Option<String> {
        if self.command.contains("=") && self.command.contains(";") {
            let keywords: Vec<&str> = self.command.split(&['=', ';']).collect();
            return Some(keywords[2].to_string());
        }

        if self.command.contains("=") && !self.command.contains(";") {
            let keywords: Vec<&str> = self.command.split("=").collect();
            return Some(keywords[1].to_string());
        }

        if !self.command.contains("=") && self.command.contains(";") {
            let keywords: Vec<&str> = self.command.split(";").collect();
            return Some(keywords[0].to_string());
        }

        return None;
    }

    pub fn jump(&self) -> Option<String> {
        if self.command.contains("=") && self.command.contains(";") {
            let keywords: Vec<&str> = self.command.split(&['=', ';']).collect();
            return Some(keywords[2].to_string());
        }

        if !self.command.contains("=") && self.command.contains(";") {
            let keywords: Vec<&str> = self.command.split(";").collect();
            return Some(keywords[1].to_string());
        }
        return None;
    }
}

code

#[derive(Debug, Clone)]
pub struct Code {}

impl Code {
    pub fn new() -> Self {
        Self {}
    }

    pub fn dest(&self, str: &str) -> u16 {
        match str {
            "null" => return 0b_0000_0000_0000_0000_u16,
            "M" => return 0b_0000_0000_0000_1000_u16,
            "D" => return 0b_0000_0000_0001_0000_u16,
            "MD" => return 0b_0000_0000_0001_1000_u16,
            "A" => return 0b_0000_0000_0010_0000_u16,
            "AM" => return 0b_0000_0000_0010_1000_u16,
            "AD" => return 0b_0000_0000_0011_0000_u16,
            "AMD" => return 0b_0000_0000_0011_1000_u16,
            _ => return 0b_0000_0000_0000_0000_u16,
        }
    }

    pub fn comp(&self, str: &str) -> u16 {
        match str {
            "0" => return 0b_0000_1010_1000_0000_u16,
            "1" => return 0b_0000_1111_1100_0000_u16,
            "-1" => return 0b_0000_1110_1000_0000_u16,
            "D" => return 0b_0000_0011_0000_0000_u16,
            "A" => return 0b_0000_1100_0000_0000_u16,
            "!D" => return 0b_0000_0011_0100_0000_u16,
            "!A" => return 0b_0000_1100_0100_0000_u16,
            "-D" => return 0b_0000_0011_1100_0000_u16,
            "-A" => return 0b_0000_1100_1100_0000_u16,
            "D+1" => return 0b_0000_0111_1100_0000_u16,
            "A+1" => return 0b_0000_1101_1100_0000_u16,
            "D-1" => return 0b_0000_0011_1000_0000_u16,
            "A-1" => return 0b_0000_1100_1000_0000_u16,
            "D+A" => return 0b_0000_0000_1000_0000_u16,
            "D-A" => return 0b_0000_0100_1100_0000_u16,
            "A-D" => return 0b_0000_0001_1100_0000_u16,
            "D&A" => return 0b_0000_0000_0000_0000_u16,
            "D|A" => return 0b_0000_0101_0100_0000_u16,
            "M" => return 0b_0000_1100_0000_0000_u16,
            "!M" => return 0b_0000_1100_0100_0000_u16,
            "-M" => return 0b_0000_1100_1100_0000_u16,
            "M+1" => return 0b_0000_1101_1100_0000_u16,
            "M-1" => return 0b_0000_1100_1000_0000_u16,
            "D+M" => return 0b_0000_0000_1000_0000_u16,
            "D-M" => return 0b_0000_0100_1100_0000_u16,
            "M-D" => return 0b_0000_0001_1100_0000_u16,
            "D&M" => return 0b_0000_0000_0000_0000_u16,
            "D|M" => return 0b_0000_0101_0100_0000_u16,
            _ => return 0b_0000_0000_0000_0000_u16,
        }
    }

    pub fn jump(&self, str: &str) -> u16 {
        match str {
            "null" => return 0b_0000_0000_0000_0000_u16,
            "JGT" => return 0b_0000_0000_0000_0001_u16,
            "JEQ" => return 0b_0000_0000_0000_0010_u16,
            "JGE" => return 0b_0000_0000_0000_0011_u16,
            "JLT" => return 0b_0000_0000_0000_0100_u16,
            "JNE" => return 0b_0000_0000_0000_0101_u16,
            "JLE" => return 0b_0000_0000_0000_0110_u16,
            "JMP" => return 0b_0000_0000_0000_0111_u16,
            _ => return 0b_0000_0000_0000_0000_u16,
        }
    }
}

main

実装したモジュール群を利用して最終的に .hack を作成するmainの実装も貼る。

use std::{
    env,
    fs::File,
    io::{BufRead, BufReader, Write},
    path::Path,
    process::exit,
};

use crate::parser::CommandType;

mod code;
mod parser;
mod symbol_table;

const ROM_ADDRESS: u16 = 0;
const RAM_ADDRESS: u16 = 16;

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() <= 1 {
        println!("require argument");
        exit(1)
    }

    let file_name = &args[1];
    let mut st = symbol_table::SymbolTable::new();
    let c = code::Code::new();

    let file = File::open(file_name).unwrap_or_else(|e| {
        println!("file open error, file name is {}, error: {}", file_name, e);
        exit(1);
    });

    let mut rom_address = ROM_ADDRESS;

    for line in BufReader::new(file).lines() {
        let line = match line {
            Ok(l) => l,
            Err(e) => {
                println!("read line error: {}", e);
                exit(1);
            }
        };

        let cleaned = match clean_line(&line) {
            Some(cmd) => cmd,
            None => continue,
        };

        let parser = parser::Parser::new(cleaned);

        match parser.command_type() {
            CommandType::ACommand | CommandType::CCommand => {
                rom_address += 1;
            }
            CommandType::LCommand => {
                process_l_command(&parser, &mut st, rom_address);
            }
        }
    }

    let file = File::open(file_name).unwrap_or_else(|e| {
        println!("file open error, file name is {}, error: {}", file_name, e);
        exit(1);
    });

    let mut ram_address = RAM_ADDRESS;
    let mut machine_codes: Vec<u16> = vec![];

    for line in BufReader::new(file).lines() {
        let line = match line {
            Ok(l) => l,
            Err(e) => {
                println!("read line error: {}", e);
                exit(1);
            }
        };

        let cleaned = match clean_line(&line) {
            Some(cmd) => cmd,
            None => continue,
        };

        let parser = parser::Parser::new(cleaned);

        match parser.command_type() {
            CommandType::ACommand => {
                let code = process_a_command(&parser, &mut st, &mut ram_address);
                machine_codes.push(code);
            }
            CommandType::CCommand => {
                let code = process_c_command(&parser, &c);
                machine_codes.push(code);
            }
            CommandType::LCommand => {
                // skip
            }
        }
    }

    let mut binary_code = String::from("");

    for machine_code in &machine_codes {
        println!("{:016b}", machine_code);
        let code_str = String::from(format!("{:016b}\n", machine_code));
        binary_code.push_str(&code_str);
    }

    let input_path = Path::new(&args[1]);
    let dest_file_name = input_path.with_extension("hack");

    let mut create_file = match File::create(&dest_file_name) {
        Err(e) => {
            println!("couldn't create .hack file : {}", e);
            exit(1);
        }
        Ok(f) => f,
    };

    match create_file.write_all(binary_code.as_bytes()) {
        Err(e) => {
            println!("couldn't write file : {}", e);
            exit(1);
        }
        Ok(_) => {
            println!("successfully wrote .hack file to {:?}", dest_file_name);
        }
    }
}

fn clean_line(line: &str) -> Option<&str> {
    let trimmed = line.trim();

    if trimmed.is_empty() {
        return None;
    }

    if trimmed.starts_with("//") {
        return None;
    }

    let code_part = match trimmed.find("//") {
        Some(idx) => &trimmed[..idx],
        None => trimmed,
    };

    let cleaned = code_part.trim();

    if cleaned.is_empty() {
        None
    } else {
        Some(cleaned)
    }
}

fn process_a_command(
    parser: &parser::Parser,
    st: &mut symbol_table::SymbolTable,
    ram_address: &mut u16,
) -> u16 {
    let symbol = parser.symbol();

    if let Ok(address) = symbol.parse::<u16>() {
        return address;
    }

    match st.get_address(&symbol) {
        Some(&address) => address,
        None => {
            let address = *ram_address;
            st.add_entry(symbol, address);
            *ram_address += 1;
            address
        }
    }
}

fn process_c_command(parser: &parser::Parser, code: &code::Code) -> u16 {
    let mut instruction: u16 = 0b1110_0000_0000_0000;

    if let Some(comp_str) = parser.comp() {
        let comp_bits = code.comp(&comp_str);
        instruction |= comp_bits;

        if comp_str.contains('M') {
            instruction = (instruction & 0b0001_1111_1111_1111) | 0b1111_0000_0000_0000;
        }
    }

    if let Some(dest_str) = parser.dest() {
        let dest_bits = code.dest(&dest_str);
        instruction |= dest_bits;
    }

    if let Some(jump_str) = parser.jump() {
        let jump_bits = code.jump(&jump_str);
        instruction |= jump_bits;
    }

    instruction
}


fn process_l_command(
    parser: &parser::Parser,
    st: &mut symbol_table::SymbolTable,
    rom_address: u16,
) {
    let symbol = parser.symbol();

    if !st.contains(&symbol) {
        st.add_entry(symbol, rom_address);
    }
}

コンピュータシステムの理論と実装の1~5章をやり直した

コンピュータシステムの理論と実装の第2版が半年くらい前に発売されて、またやりたいなーと思ってここ2,3日の空き時間を使ってちょこちょこ進めていた。

いったんハードウェア編の1~5章まで終わったので、ALUとMemoryとCPUだけこのブログに残しておこうと思う。

ちなみに第2版は購入していないので、第1版をベースに進めた。

ALU

第2章のALUを実装してみる。

プロセス

ありがたいことに書籍の中に擬似コードが記載されているので、その流れに沿って論理回路を組み立てていけば作成できる。

擬似コードは下記のようになっている。

入力 : x[16], y[16], zx, nx, zy, ny, f, no

出力 : out[16], zr, ng

関数 : 

if zx then x = 0
if nx then x = !x
if zy then y = 0
if ny then y = !y
if f then out = x + y
      else out = x & y
if no then out = !out
if out = 0 then zr = 1 else zr = 0
if out < 0 then ng = 1 else ng = 0

コンピュータシステムの理論と実装の35ページより引用

上記の擬似コードを分割して、分割されたロジック1つひとつについて考えてみる。

x[16]の入力値がfの関数に渡されるまでの流れ

zxの値によって入力値xをそのまま利用するかxを0にするかを決定する必要があるので、Mux16のゲートを利用することで値を決定することができる。

nxの値によって先ほどのMux16ゲートの出力値を利用するか、Mux16ゲートの出力値を反転させた値を使うかどうかなので、事前にNotゲートで反転させた結果を持っておく。 先ほどと同様にMux16ゲートを利用し、zxをselとして利用したMux16ゲートの出力値とNotゲートによって反転させた値を入力値として、出力値を得ることで、fを利用した演算に利用する入力値xを決めることができる。

y[16]の入力値がfの関数に渡されるまでの流れ

前述の「x[16]の入力値がfの関数に渡されるまでの流れ」と全く同じロジックになるので、割愛する。

fとnoを利用した演算結果の流れ

xの値とyの値は決まったので、この2つの値を用いてあらかじめAnd演算と加算を行う必要がある。演算で得られたそれぞれの結果をMux16ゲートの入力値として利用することで、outの出力値を決めることができる。

noは出力されたoutという値を反転させるかどうかに利用される値である。Mux16ゲートを利用してoutの最終的な値を決めたい。入力値として存在しないものは先ほどの演算で出力されたoutを反転させた値だけなので、Notゲートを用いて反転させた値を作成し、Mux16ゲートに入力値として渡せばいい。

zrとngの値を出力する流れ

まずは、zrについて考えていく。

outの値が0のときにzrに1という値が出力されるようにしたい。outの値が0かどうかを判定するためには、それぞれのbitでOr演算することで判定することができる。第1章でOr8Wayという多入力ゲートを作ったのでこのゲートを利用していく。 Or8Wayは8bitの入力のうち、少なくとも1つでも1が存在すれば出力結果を1とするゲートである。入力値のすべてのbitが0ならば、Or8wayの結果が0になる。Or8Wayの結果をNotゲートで反転させることで、zrの期待する値を出力することができる。ALUで扱っているのは16bitの値なので、前半8bit, 後半8bitをそれぞれOr8Wayゲートの入力に利用して、出力値をOr演算させて、Not演算させることで期待するzrの値を出力することができる。

次に、ngについて考えていく。

これは新たに論理回路を構築する必要がない。outの値が負の数ならば最上位bitが1になるので、outの値の最上位bitをそのままngの値として利用することで期待するngの値を出力することができる。

回路図(メモ書き)

最終的な回路図は下記のようになる。

※メモ書きなので、記号などかなり雑であり、図として厳密性に欠けることに注意してほしい。

HDL

最終的にできたHDLは下記のようになる。

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit inputs        
        zx, // zero the x input?
        nx, // negate the x input?
        zy, // zero the y input?
        ny, // negate the y input?
        f,  // compute (out = x + y) or (out = x & y)?
        no; // negate the out output?
    OUT 
        out[16], // 16-bit output
        zr,      // if (out == 0) equals 1, else 0
        ng;      // if (out < 0)  equals 1, else 0

    PARTS:
    
    Mux16(a=x, b[0..15]=false, sel=zx, out=outXor0);

    Not16(in=outXor0, out=notOutXor0);

    Mux16(a=outXor0, b=notOutXor0, sel=nx, out=outX);

    Mux16(a=y, b[0..15]=false, sel=zy, out=outYor0);

    Not16(in=outYor0, out=notOutYor0);

    Mux16(a=outYor0, b=notOutYor0, sel=ny, out=outY);

    And16(a=outX, b=outY, out=XandY);

    Add16(a=outX, b=outY, out=XaddY);

    Mux16(a=XandY, b=XaddY, sel=f, out=outF);

    Not16(in=outF, out=notOutF);

    Mux16(a=outF, b=notOutF, sel=no, out=out, out[0..7]=o1, out[8..15]=o2, out[15]=ng);

    Or8Way(in=o1, out=w1);
    Or8Way(in=o2, out=w2);
    Or(a=w1, b=w2, out=w3);
    Not(in=w3, out=zr);

}

Memory

第5章のMemoryを実装してみる。

プロセス

MemoryはRAM(16K)とキーボードとスクリーンの3要素から構成される。

RAM(16K)、スクリーン、キーボードのメモリアドレスはあらかじめ決められている。入力のアドレスによって入力値をどこに書き込むのかを決める必要がある。

  • RAMのアドレスは0~16383
  • スクリーンのアドレスは16384~24575
  • キーボードのアドレスが24576

RAMのアドレスの16383を2進数に変換すると 11111111111111 という14bitのアドレスになる。(15bitの場合、最上位の2bitは01になる)

スクリーンのアドレスの16384を2進数に変換すると 100000000000000 という15bitのアドレスになる。(最上位の2bitは10になる)

24575も同様に変換すると、 101111111111111 というアドレスになる。

キーボードのアドレスの24576も同様に変換すると、 110000000000000 という15bitのアドレスになる。(最上位の2bitは11になる)

下記のようにまとめることができる。

address[14] address[13] どれを使うか
0 0 RAM
0 1 RAM
1 0 SCREEN
1 1 KEYBOARD

RAMに書き込むかどうかを決定するloadという入力値を決めて、RAMに渡さなければならない。上記の表よりRAMにわたす入力値のloadは、address[14]を反転させて1になる場合かつ、Memoryに渡される入力値のloadが1の場合である。

RAMと同様にSCREENに書き込むかどうかを決定するloadの入力値を決める必要がある。上記の表よりSCREENにわたす入力値のloadは、address[13]を反転させて1になる場合かつ、address[14]とのAnd演算の結果が1になる場合かつ、Memoryに渡される入力値のloadが1の場合である。

RAMもしくはSCREENのどちらに入力値を書き込むのかを決定できるようになった。最終的にMemoryの出力値を選択しなければならない。上記でまとめた表は4入力マルチプレクサ(Mux4Way16)の表と全く同じ構成になる。このゲートを利用して最終的なMemoryの出力値が決まる。

回路図(メモ書き)

HDL

CHIP Memory {
    IN in[16], load, address[15];
    OUT out[16];

    PARTS:
    Not(in=address[14], out=notaddress14);
    And(a=load, b=notaddress14, out=ramload);

    Not(in=address[13], out=notaddress13);
    And(a=address[14], b=notaddress13, out=screencondistion);
    And(a=load, b=screencondistion, out=screenload);

    RAM16K(in=in, load=ramload, address=address[0..13], out=ramout);
    Screen(in=in, load=screenload, address=address[0..12], out=screenout);
    Keyboard(out=keyout);
    
    Mux4Way16(a=ramout, b=ramout, c=screenout, d=keyout, sel=address[13..14], out=out);
}

CPU

第5章のCPUを実装してみる。

プロセス

ありがたいことに書籍の中に回路図の全体がヒントとして記載されている。(102ページの図5-7に記載されている)

このあらかじめ用意された回路図をベースに1つずつ論理回路を構築していく。(書籍に定義されている入力や出力をはじめとした前提となる情報の説明は省略する)

Aレジスタに値を格納するまでの処理

回路図を見ると、Aレジスタにの前段階に設置されるMuxゲートのselとAレジスタに書き込む際のloadの値が決まっていないので、この2つの値を決めていく。

HackプラットフォームはA命令とC命令から成り立ち、A命令の場合は最上位bitが0、C命令の場合は最上位bitが1と定義されている。Aレジスタに書き込む前のデコードのプロセスが存在する。このプロセスで入力値として渡されたinstructionを読み取り、A命令もしくはC命令かを判別する。

A命令であれば、instructionで定義されたメモリアドレスをAレジスタに格納し、C命令であれば、CPUの前回の演算結果をAレジスタに格納するので、デコード後に待ち構えるMuxゲートのselの値に関する真理値表は下記のようになる。

instruction[15] Sel
0 1
1 0
Sel Out
0 a
1 b

※ MuxのinputのaにCPUの前回の演算結果を、bにinstructionを入力するとする

上記より、instruction[15]の値を反転させれば、Muxで利用するSelの値が決まる。

続いて、Aレジスタに書き込むかどうかを決めるのに必要なloadの値を決めていく。

A命令の場合、渡されたinstructionをAレジスタに書き込むことになるが、C命令の場合、dest領域の値によってAレジスタに書き込むかどうかが決まってくるため、場合分けして考えなければならない。

dest領域の仕様は下記の真理値表になる。

d1 d2 d3 ニーモニック
0 0 0 null
0 0 1 M
0 1 0 D
1 0 0 A
0 1 1 MD
1 0 1 AM
1 1 0 AD
1 1 1 AMD

ニーモニックにAが含まれていればAレジスタに書き込むという意味である。つまり、Aレジスタに書き込む場合 d1 = 1 でなければならない。

Aレジスタに書き込むかどうかを決めるloadの真理値表は、instruction[15]の値とd1の値を用いて作成することができ、下記のようになる。

instruction[15] d1 load 備考
0 0 1 A命令
0 1 1 A命令
1 0 0 C命令
1 1 1 C命令

loadの値は、instruction[15]を反転させて、d1とAnd演算の出力値をとればいいということになる。この回路図(メモ書き)は下記のようになる。

※ i は instruction[15]

ALUのインプットに利用する値が決まるまでの流れ

ALUにわたす入力のうちの一方はDレジスタに格納された値で、もう一方はCPUの入力値として渡されたinMもしくはAレジスタの値である。

後者に関して、回路図全体だと、AレジスタとALUの間にいずれかの値を利用するかを決定するMuxゲートが存在するので、このMuxゲートのSelの値を決めていく必要がある。

C命令の仕様を確認すると、a(comp領域の最上位ビット)が0の場合Aレジスタを利用し、1の場合M(Memory)の値を利用するように定義されている。

つまり、selを決める真理値表は下記のようになる。

instruction[15] a (instruction[12]) sel 備考
0 0 0 A命令
0 1 0 A命令
1 0 0 C命令
1 1 1 C命令

instruction[15]と a(instruction[12]) のAnd演算の出力結果をselの値として利用することで、ALUのyの入力値を期待通りに決めることができる。

Dレジスタへの書き込むかどうかを決める流れ

ALUの処理結果はMemoryもしくは、Aレジスタ、Dレジスタのいずれかに書き込む仕様になっている。 Dレジスタに毎回書き込むわけではないので、Dレジスタに書き込むかどうかを決めるloadの値を作成する必要がある。

Dレジスタのloadの値を作成する考え方は、前述のAレジスタの入力値として渡すloadを決めるときの考え方と同じである。 C命令かつC命令のdest領域のd2の値が1のときにDレジスタに書き込むので、真理値表はinstruction[15]とd2(instruction[4])から作成することができ、下記のようになる。

instruction[15] d2 (instruction[4]) load 備考
0 0 0 A命令
0 1 0 A命令
1 0 0 C命令
1 1 1 C命令

And演算を用いることで、Dレジスタに値を書き込むかどうかを決めるloadの値を作成することができる。

writeMの値を決めるまでの流れ

CPUの出力結果をMemoryに書き込むかどうかを決めるwriteMの値を作成するときの考え方は、AレジスタのloadとDレジスタのloadを決めるときの考え方と同じである。

instruction[15]とd3(instruction[3])のAnd演算の結果がそのままwriteMの値となる。

ALUのxとy以外の入力値

本書より、C命令のcomp領域の仕様とALUの仕様が一致するので、zx, nx, zy, ny, f, noの値は、instruction[6..11]の値をそれぞれあてはめればいい。

PCの入力値(load)の決定

C命令のjump領域とALUの出力結果のzrとngの値を利用していく。

真理値表は下記のようになる。

zr ng j1 j2 j3 jump
0 0 0 0 0 0
0 1 0 0 0 0
1 0 0 0 0 0
0 0 0 0 1 1
0 1 0 0 1 0
1 0 0 0 1 0
0 0 0 1 0 0
0 1 0 1 0 0
1 0 0 1 0 1
0 0 0 1 1 1
0 1 0 1 1 0
1 0 0 1 1 1
0 0 1 0 0 0
0 1 1 0 0 1
1 0 1 0 0 0
0 0 1 0 1 1
0 1 1 0 1 1
1 0 1 0 1 0
0 0 1 1 0 0
0 1 1 1 0 1
1 0 1 1 0 1
0 0 1 1 1 1
0 1 1 1 1 1
1 0 1 1 1 1

上記の真理値表より下記のように整理できる

  1. zrとj2が両方1の場合、jumpが1
  2. ngとj1が両方1の場合、jumpが1
  3. zrとngが0でj3が1ならば、jumpが1

jumpが1になるには、1と2に関して2つの値のAnd演算の出力結果を用いればよく、3に関してzrとngが両方0かどうかを判定しなければならないので、Or→Not→Andの順に回路を作る必要がある。

1, 2, 3のそれぞれの演算の結果、いずれか1つでもjumpが1ならば、PCで利用するloadが1になるので、Or演算を用いることでloadの値を決めることができる。

最終的なPCのloadを決める回路(メモ書き)は下記のようになる。C命令の場合のみに適用されることを忘れてはならない。

補足(A命令の時のALUの演算結果は?)

A命令の場合、Aレジスタ以降の処理及び処理結果がどうなるのか?と思うかもしれないが、Dレジスタへの書き込みに利用するloadもMemoryへの書き込みに利用するwriteMもC命令が前提となってそれぞれの値が決まるため、A命令の場合はこれら2つの値が1になることはない。なんらかALUで計算され、結果が出力されるかもしれないが後続の処理に影響を及ぼすことはない。

回路図

書籍に記載されているので、割愛する。

HDL

CHIP CPU {

    IN  inM[16],         // M value input  (M = contents of RAM[A])
        instruction[16], // Instruction for execution
        reset;           // Signals whether to re-start the current
                         // program (reset==1) or continue executing
                         // the current program (reset==0).

    OUT outM[16],        // M value output
        writeM,          // Write to M? 
        addressM[15],    // Address in data memory (of M)
        pc[15];          // address of next instruction

    PARTS:
    Not(in=instruction[15], out=mux1sel);

    Mux16(a=previousOutValue, b=instruction, sel=mux1sel, out=mux1out);

    Not(in=instruction[15], out=notInstruction15);
    Or(a=notInstruction15, b=instruction[5], out=loadRegisterA); // instruction[5] = d1

    ARegister(in=mux1out, load=loadRegisterA, out=registerAout, out[0..14]=addressM);

    And(a=instruction[12], b=instruction[15], out=mux2sel);
    Mux16(a=registerAout, b=inM, sel=mux2sel, out=mux2out);

    And(a=zr, b=instruction[1], out=zeroJump);
    And(a=ng, b=instruction[2], out=minusJump);

    Or(a=zr, b=ng, out=plusValue);
    Not(in=plusValue, out=notPlusValue);
    
    And(a=notPlusValue, b=instruction[0], out=jumpPositiveValue);
    
    Or(a=zeroJump, b=minusJump, out=jumpNegativeValue);
    Or(a=jumpNegativeValue, b=jumpPositiveValue, out=pcLoad);
    And(a=pcLoad, b=instruction[15], out=writePc);

    PC(in=mux2out, inc=true, load=writePc, reset=reset, out[0..14]=pc);


    ALU(x=registerDValue, y=mux2out, zx=instruction[11], nx=instruction[10], zy=instruction[9], ny=instruction[8], f=instruction[7], no=instruction[6], out=outM, out=previousOutValue, zr=zr, ng=ng);

    And(a=instruction[15], b=instruction[4], out=loadRegisterD);
    DRegister(in=previousOutValue, load=loadRegisterD, out=registerDValue);
    And(a=instruction[15], b=instruction[3], out=writeM);

}

まとめ

久しぶりにHDLを書いたのは楽しかった。 2進数だけで条件分岐させたり、ロジックを組み立てるときの頭の使い方が最初始めたときになかなか出てこなくてちょっと時間を溶かしてしまった。 CPUの創りかたを読み直したことで、真理値表に落とし込んでパターンを探って解を見つけていくんだというプロセスを思い出せて、ロジックを組み立てることができた。論理回路を作るプロセスはパズルゲームっぽい感じがあり楽しいものだと改めて実感できた。

6章以降のソフトウェア編も引き続き実装しているのでキリの良いタイミングで更新していこうと思う。

「自分を変えなければいけない」と気づくために

「自分を変えなければいけない」と気づくためにはどうしたらいいのだろうか?

周りが変えようと思ってアドバイスしたり、時には厳しく言ったとしても気づくことはほとんどない。今なら少しわかるのだけど、ほとんどの場合、上司は自分よりも広い世界を見ることができる。だから自分より多くのコンテキスト情報を持ってアドバイスやフィードバックを言ってくれている。

それに対して受け入れることができないのは、素直ではない。

よくあるのは他責にしてしまうことだけど、結局のところ自分が世界を見ることができているという間違った前提に気づくことができていない。

表面には出てこないし、本人も気づけていないんじゃないかと思うがコンフォートゾーンから抜けるのが怖いのではないかのではないかと思っている。

怖いからこそ課題や改善のベクトルを他者や周囲の環境に向けてしまうのではないか?

自身のわずかな経験で思ったのはハードスキルの獲得によって給料は上がるかもしれないが人間的な成長は小さいかもしれない。人間的な成長を求めるのであれば苦手なことやコンフォートゾーンから抜け出してチャレンジする。そして自分のできなさを受け入れることができて成長のスタート地点に立つことができる。つまり自分を変えなければいけないと気づくことに立つことができると思う。

※ハードスキルの獲得も苦労するものなので成長がないわけではない

期待を超えるためには自身であるべき姿を描く必要があった

※自身の最近の反省をつらつらと書きました。

エンジニアリングマネージャー的なロールになり、エンジニアを評価する側になり、CTOの期待以上に働けている人とそうでない人の違いはなんなんだろうと考えていた。

期待以上に働くためにはあたり前だが、その人の想定している範囲外のアクションや成果が必要だったりする。

その人の想定している範囲内での活動では期待を超えることができない。

期待を超えて働くためには、時間軸を今まで以上に長く持ち自分の中であるべき姿やWillを描いて実行する必要があると思う。

私は、去年の1年間エンジニアの評価制度を作成して導入したり、1on1をはじめてみたり、目標設定を導入して運用したりしていた。(今、すでに終わっているわけではなくてまだまだ改善の途中である)

今日まで走ってみて期待を超えるというところまでは至っていないと思っている。

この活動が理想通りに行っていないところはもちろんあるが、これらの活動に留まってしまったということが問題だと思っている。

これらの活動はもともと、エンジニアリングマネージャーを始めるときに期待値調整的に話したことだからだ。

活動1つひとつの質をかなり上げて、成果にも現れてくれば期待を超えたと言えるかもしれないが、エンジニアリングマネージャーとして本当にそれでいいのだろうか??

なぜこう思うのかというと、これらの活動というのは、エンジニアリングマネージャー目的・ミッションである組織のアウトプットを最大化するためのアプローチの一部を最適化しているに過ぎないからだと最近思っているからだ。

エンジニアリングマネージャーとして本来のミッションを追うために、組織全体に対してもっと大局的に自分なりのあるべき姿を持つ必要があった。

そして、それを周囲にぶつけながらも前進させなければならなかった。

描くだけはできるかもしれないが、ぶつけるということは自身も批判されたりと痛みを伴う。

そのような痛みもあるし、大局的にあるべき姿を描いて実行するということは大きな責任も伴う。

だから自身で腹をくくるというイベントがないと大局的に描いて実行することができない。期待を超えるスタート地点に立つことができない。

時間はかかったが、最近腹をくくることができた。動きも変わってきた。(むしろ変えたと言ってもいい。)

腹をくくることができたことで、期待を超えるために必要なことがどんなことなのかに気づくことができた。

EMの役割を自分なりに定義してみる(前編)

この記事は Engineering Manager Advent Calendar 2023の21日目の記事です!

はじめまして、yokishavaといいます。 EMというロールをはじめて1年が経とうとしています。

このポストではEMの役割というものを自分なりに言語化してみようと思います。 背景としては、EMとして取り組んでいることに対して、以前「どこに向かっているんだっけ。。?」「この取り組みはどこに作用させようとしているんだっけ?」とわたしが疑問を思ったことがあったからです。 マネジメントに関する本を読んで、私自身EMの役割を理解しているつもりではありましたが、それがいつの間にか自身の中から忘れられてしまっていることに気が付きました。 自分なりに言語化すること、このブログに残すことで、今後同じようなことが発生したときも立ち戻れるようにしたいと考えています。

補足

EMといっても各社で求められる役割や取り組んでいる内容は異なるかと思います。 わたしは、あるプロダクトチームに所属してEMというロールを全うしているというよりは、各プロダクトチームを横断して、エンジニアがパフォーマンスを最大化し続けられるようにすることを目指しています。 実際にわたしは、下記のようなことに取り組んでいて、様々あるマネジメント領域でもピープルマネジメントの比重が大きいと認識しています。

  • エンジニア採用(予算計画やダイレクトスカウト、面談...etc)
  • エンジニア向けの人事制度の作成と運用
  • 目標設定の導入や推進
  • 1on1の実施 など(ちょっと人事っぽいですね。)

※ピープルマネジメントに偏った取り組みを日々行っているため、読者の方によってはこれから書かれている内容に漏れがあると思うかもしれません。 ※自身のことを初心者EMだと認識しています。これから書かれている内容に対してベテランEMの方は「ここが見えてないぞ」と思うかもしれません。読んだ後にアドバイスをいただけるととても喜びます。

EMの役割とは

先に結論から書いていこうと思います。私にとってのEMの役割とはこちらになります。

「エンジニア組織がパフォーマンスを最大化し続けられるようにすること」

この言葉にした理由を今回は書いていきます。 この言葉は裏を返すと、最大化し続けることを揺るがす何かがあります。 その何かは変化であり、大きく2つの変化に分けることができるとわたしは思っています。

1つは人の変化です。環境など目の前のことをどう解釈するかで個人のパフォーマンスは変わってくると考えています。 2つ目は、事業や組織の変化です。プロダクトは成長すればフェーズが当然変わってきますし、それに応じて人が増えたり、チームの状況も少しずつ変わってきます。それらの要因を見極めながら対応しなければパフォーマンスはたちまち変わってきてしまうと思います。

今回のポストでは前編として1つ目の人の変化について書いて、後編で2つ目の事業や組織の変化について書こうと思います。

人の変化について

1人のパフォーマンスは今置かれている状況と自己実現欲求のつながりの解釈によって変化する

時間軸は人それぞれ異なるにせよ個人は、「これをやりたい」「こうなりたい」などと何かしらの欲求を持っています。 所属している会社で、今会社で個人が取り組んでいることとその欲求が一致していたらそれはモチベーション*1ややる気など上がるかと思います。もしくは夢中になって取り組むことができる人もいるでしょう。それはとてもパフォーマンスが発揮されやすい状態になります。 しかし、そうではない場合どうでしょうか? 個人の欲求と全く一致しないようなことを取り組んでいるとしたら、モチベーションは下がり、パフォーマンスが発揮されにくい状況になってしまうのではないでしょうか?

個人が置かれている状況をどう解釈するかで感情が揺さぶられ、パフォーマンスに影響を及ぼしてしまいます。

「じゃあその人がやりたいことをやらせてあげよう」「その人がやりたいお仕事を提供してあげよう」と言いたいわけではありません。(そのようなケースがあるとは思いますが。。)

もしかしたら、マネージャーがしっかり期待を伝えていない、伝えていたとしてもそれを理解していなかったのかもしれません。 個人がどんなことをやりたいのかマネージャーは理解していたつもりですが、もう少し深ぼってみると理解が異なっていたかもしれません。 まずは対話することによってどこに原因があるのか探ることから始めるのがいいのではと考えています。

ジョハリの窓を参考に「自身が知っていて相手が知らない」「自身が知らなくて相手が知っている」これら2つの領域に存在しているものを把握し、「自身も相手も知っている」という領域を大きくしていくことで相互理解*2*3が深まり、パフォーマンスが発揮しやすい心理的な状況を生み出しやすくなります。

「あなたはまだ見えてないかもしれないけどxxxという強みがあるからやってみては?」「中長期的にxxxというようになりたいならば、yyyというのは実はこうつながっているよ」「xxxという取り組みはyyyという意義があるよ」などのコミュニケーションをマネージャーはできるようになり、個人が「やりたいこと」と会社が「期待していること・やってほしいこと」2つの重なりを大きくできるようになります。

どう解釈するかは個人が悩んだり、考えたりする中で変化させることはもちろんできると思っていますが、第三者との対話を通して見えていなかったことを理解することがより効果的であると考えています。 対話などを通して、目の前の状況に対して個人がいい解釈を行えるように支援して、個人がパフォーマンスを発揮しやすいような状況に持っていくことがEMの役割の1つだと考えています。

1人の持っている能力は変化する

モチベーションに関連のありそうな問題を解決することができたとしてもそれではまだ足りないと思っています。 能力に焦点をあてていないからです。 個人がパフォーマンスを出すために足りてない能力があれば、それを習得しようとします。 個人がパフォーマンスを出すための能力をすでに持っていたとしても新しいチャレンジをしてパフォーマンスを出せるようにするために、足りてない能力を習得しようとします。 能力はチャレンジを伴う取り組みと時間の経過とともに蓄積されていきます。 その能力を発揮することができたとき、会社にとっての成果にも結びつき、活躍したという状態になります。

個人が能力を獲得する・発揮できるようにし、なるべく早く「活躍している」という状態に持っていき、そのサイクルをできる限り続けられるようにすることもEMとしての役割の1つだと考えています。

ある組織に所属するという文脈で人にはライフサイクルが存在しているとわたしは考えています。

入社して、チャレンジをして成長を通して活躍していき、チャレンジ〜成長〜活躍が繰り返され、もしかしたらいつか退社という状態に行き着きます。 このライフサイクルを考えながら現状どの状態にいてどうしたら次の状態に移行できるのか考えています。 観察などを通して「活躍している」という状態に持っていくためにはどこがボトルネックになっているのか?という問いかけてみます。

  • 能力が足りていないのか?
  • 能力を発揮する方向性を間違えてしまっているのか?
  • 今のチーム状態ならば、チームとして足りてない機能はAではなくBという能力ではないのか?
  • 能力を発揮する状態までに至っていないのか?

例えば、新しく入社した方が技術的には十分な能力を持っていたとしても、新しい事業ドメインの理解に苦しみ能力を十分に発揮することができていないかもしれません。新しく入社した方が新しい事業ドメインを理解できるようにオンボーディングを改善したりするなどが考えられるかもしれません。

ライフサイクルの図にあるようにいかに「活躍」という状態に持っていくか、さらなる成長のために次にチャレンジする機会や場を創り、「活躍」のサイクルを繰り返し回せるようにするかということもEMとして重要な役割だと思っています。

前編のまとめ

自分なりに定義したEMの役割に対しての理由を書いてみました。 EMは1人の人物を俯瞰的に見ることが求められるのだと、私は書きながら改めて思いました。 後編はEMの役割の理由について事業や組織の変化という観点から書いていきたいと思います。(役割に「し続ける」という表現を用いたのも後編で書くことになりそうです)

それでは明日のブログもお楽しみに!

*1:モチベーションで仕事をするなという意見もあると思いますが今回は触れません。

*2:言われたことを受け入れてみる姿勢を持っていることも前提として必要と思ってます。

*3:前提として信頼関係の構築も必要と思ってます。

`Engineering Manager Meetup #11` に参加しました

数年ぶりにオフラインのイベントに参加してきました!

engineering-manager-meetup.connpass.com

参加したきっかけ

今まさに評価制度を社内で作ろうとしていて、わからないことも多かったり、

自身がチームビルディングをうまくできてないという実感があって、周りの方々はどんな動きをしているのか率直に知りたいと思っていたところに、このイベントを見て、早速申し込んで参加しました。

どんなイベントだったか

今まではLTだったりと数人が登壇して資料を用いて発表する人と聴講する人みたいな形式みたいなイベントしか参加したことがなかったんですが、

このイベントは、OST(オープン・スペース・テクノロジー)という形式で進められてました。

ja.wikipedia.org

参加者の自発的な発言がベースとなって進められる議論の場のため、参加してみて発表会の聴講者のような比較的インプットの姿勢とは別のスタンスでとても新鮮でした!

セッションの前半後半で本当に様々なテーマがあがっていました。

  • チームマネジメント
  • 1on1
  • 評価
  • EMの働き方の定義
  • EMのマネジメントの責任分界点
  • うまいFBの方法
  • 開発のROI
  • 意思決定にMVVをどう利用するか
  • 組織生産性のための取り組み
  • 自己組織化したチームにするための取り組み
  • より良いキャリアゴールを設定するためのためのベストプラクティス

※イベント時のテーマ名から少し言葉を省略したりしてます

後半のセッションで「自己組織化したチームにするための取り組み」をテーマにあげてセッションオーナーをさせていただきましたが

どのテーマも最後のまとめの発表だけではなく、議論に参加したくなるような興味をそそられるテーマばかりでした!

参加したテーマ

目標・評価における開発マネジメント定量指標をどうしているか

冒頭でも書いたように評価制度を作るところに携わろうとしている身だったので、このテーマに参加しました。

各社どのように定量評価をしているのか/しようとしているのか、やってみて難しいことなどなどディスカッションしました。

(ふせんに書いていただいてありがとうございました!)

最終的に、定量評価しなくてもいいんじゃないか〜みたいな結論になりました。

valueに紐付いた行動指針だけではなく、成果や能力も見た上で評価していきたいが、どう定義するのかやどんな指標を設定するのかというところは本当に苦しんでいるんだなぁと感じました。

定量評価をどうやるのか以外にも評価者は誰が担当するのか、キャリアラダー設定しているのか、目標設定をどう成長と結びつけてマネジメントするかまで評価から話が色々と広がったかもしれませんが、社内での失敗談やリアルな話もありました。これから評価制度を作っていく上でアンチパターンの1つとして知ることができたと思ったので、とてもよかったです。

自己組織化したチームするための取り組み

これは私自身が、セッションオーナーとしてテーマをあげさせていただきました。

昨年からスクラムを社内で進めたり、今まで以上にチーム全体にベクトルを向けて、チームビルディングみたいなところをやってきたのですが、 なかなかうまくいってない実感や難しさを感じていたので、どんな取り組みをしているのかを知りたかったという背景でした。

自身があげた議題だったので、とてもたくさんの知見や経験、アドバイスを聞けたのでとても勉強になったのですが、

セッションオーナーというよりかは参加いただいた諸先輩方に本当に悩みを聞いてもらい、相談にのっていただいた30minになってしまいました。

(ふせんに書いていただいてありがとうございました!)

(忘れないように...)こんなことを話しました。

  • チームはどこに向かっているのか全員で共有しているのだろうか?
  • そのチームはなんでそのメンバーで組成されたんだろうか?
  • メンバーはどんなところに関心があったり、どの領域にベクトルが向いているのだろうか?

課題を深堀りしていくと、チームの目的や目標、Whyみたいな部分の共有がとても弱かったことがわかりました。

加えて、メンバーそれぞれ興味・関心のベクトル(例えば、「顧客課題を解決したいのか」「事業を大きくしたいのか」「技術そのもの」など)が違いますが、一人ひとりのパーソナリティをあまり理解していなかったと感じました。

それらを共有したり、知るためにどんなことをやっているのかというところで、

などなどの方法をやっているという話があがりました。

それらは知ることよりも、実際どう取り組んでいるかというところを詳しく聞けたのが目から鱗でした。

覚えているものだと、

  • インセプションデッキを作る際に、経営と現場の齟齬をなくすために、社長も巻き込んで1日使って作っている
  • MTG以外でチームの時間を毎回決めて作っている(例えば、3時に必ず休んで雑談などをするみたいな)
  • 顧客の声をチャットツールに流したり、動画を撮影したりしてどの職種の人も情報の質や量、粒度に差異が少なくする

などなど今まで実践していなかったものもあれば、実践したことはあっても中途半端やあっさりで終わったり、続いていなかったりしたものもありました。

もっとしっかり取り組まなければならないんだと振り返ることができました。

議論が進む中で、共有する際のコミュニケーションの方向が、トップダウンなのか双方向なのかということも話しの中であがり「メンバーが意見やFBを気軽に言えるような関係は構築できているのだろうか?」という領域にも広がっていきました。

FBをあげてもらうことがとても難しく感じている中で、どんなメンバーが誰に対してもFBを言えるような文化形成をできているのはとても強力だということを実感しました。

個人としてもやれる方法論はいくつもあると思いますが、会社の文化にもかなり依存するものだと思っていて、そのことを改めて確認することができたような気がしています。

懇親会でもFBの話は盛り上がったのですが、「スキがある人がいることでFBがしやすくなる」という名言も生まれたのが強く頭の中に残っています!

エンジニアリングマネージャーが感じていたり、考えているチームの課題というのは、実はメンバーが同じくらい自覚することが難しく、それを理解してもらい、チーム改善するために色々と考えて実行されているんだと学びがありました。

「向かっている理想像に到達する階段の一段は大きいので、その一段をいくつにも小さく分けて敷いてあげることがエンジニアリングマネージャーの仕事」という言葉が個人的にとても印象に残りました。

セッション後の懇親会

セッション後の懇親会も参加したのですが、セッション延長線かと思うくらいみなさんの色々な悩みも含めて色々な議論が飛び交って、これまた濃い時間だったと思いました!

どんなことを話したか

EMも開発するかどうか

もう開発にはあまり関わっていないような方もいれば、がっつり開発もしている方もいたり、MTGで一日が埋まっていたりと色々な事例がありました。

組織にも依存するところはあるとは思いますが、EMが開発もしている背景は評価する立場として、メンバーから認められてないと厳しいという意見がありました。

ただ、メンバーの中には技術的スキルで採用されたメンバーもいるため、チームで1番技術に長けているスーパーマンである必要はなく、評価もその人の立場になってできているということで、衝突が起こらないようにEMの役割やふさわしい姿を考えられて定義されているのだと感じました。

EM自身のマネジメント

日々EMはメンバーのモチベーションマネジメントなどをサポートしていますが、じゃあEM自身のモチベーションやストレスに対するマネジメントはどうするのか?ということも話があがりました。

Slackなどにマイナスな発言をしてしまうとチームの士気を下げる恐れがあり、簡単に吐き出すことはできません。他部署のマネージャーと横のつながりを作って吐き出せるようにしたり、EMの上司に当たる人物に相談していたり、色々なやり方で対応されていました。

一方でメンバーからのアラートは早く摘みたいので、普段から直接伝えていたり、Slack Reacji Channelerでそれらの問題を解決するための人たちが所属するチャンネルに流す仕組みを作っている、日報を書くようにしているなど各社様々な取り組みをされていて勉強になりました。

最後に感想

オフラインイベント自体がかなり久しぶりだったので、とても新鮮な気分になりました。

はじめてOST形式でディスカッションをしましたが、セッションオーナーとして手をあげることでフォーカスが自身の関心ごとに向くため学びがかなりあったので、手を上げてテーマを出してよかったなと思いました。

セッションでも、懇親会でもEMの先人の方々の生の声を聞くことができたのは大きな学びとともに、とても刺激的でした。

このイベントで得た知見を風化させないためにも普段の仕事でまたいろいろと考え、実行してみたいと思います!

次の機会があったときは今以上に学びを得て議論に参加できればと思いました!(早くもあれもこれも聞いてみたかったとなってますが...)