{"action":"created","comment":{"url":"https://api.github.com/repos/google/kythe/pulls/comments/208761127","pull_request_review_id":144636164,"id":208761127,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIwODc2MTEyNw==","diff_hunk":"@@ -0,0 +1,145 @@\n+---\n+title: Kythe Columnar Serving Format\n+date: 2018/08/08\n+link: https://github.com/google/kythe/pulls/2909\n+---\n+\n+# Background\n+\n+The current Kythe serving data format is just a simple key-value store from string keys (usually [Kythe tickets](https://kythe.io/docs/kythe-uri-spec.html)) to ProtocolBuffer message.  This format requires the Kythe PostProcessor to perform a complex reduction over a potentially massive number of decorations/references/edges/etc. to produce a single lookup value per node.  A set of CrossReferences can be so large that it already requires manual paging, another complex transformation in the PostProcessor.  These operations tend to create hot shards and stragglers, leading to long post-processing times and potentially failures due to memory exhaustion.  Likewise, the server is forced to slowly decode large ProtocolBuffer messages even when only a subset of the data is required and the large value sizes can cause issues with some backend stores.\n+\n+# Proposal\n+\n+Split serving table ProtocolBuffer values into their component fields, making each an independent key-value entry.  Single lookup API calls become scans over a key prefix; paging becomes trivial.  Due the characteristics of LevelDB/SSTables, the split key-value entries will most likely be read together (see SSTable block encoding), keys will be prefix-encoded, and there should be no significant performance hit for reads.  On the contrary, since the Kythe API gives users the ability to request partial data, the split key-value entries allows the server to possibly skip over large segments of data while scanning and save the cost of decoding for a performance improvement.  Examples include a user requesting only small span of a file's decorations or a user requesting only a single kind of cross-references.\n+\n+More importantly, keeping serving data separate lets the Kythe PostProcessor do fewer large joins.  There will no longer need to be a join of all file decorations to create the single `FileDecorations` proto (likewise for CrossReferences).  Fewer large joins avoids stragglers in the PostProcessor and removes the need for many \"limiters\" (i.e. cutoff values for the size of outputs).\n+\n+The scope of this proposal only includes FileDecorations and CrossReferences.  Other services could be made to use similar formats, but the most improvement will be gained from these two APIs.\n+\n+# Implementation\n+\n+Essentially each field of an existing ProtocolBuffer value in the serving data becomes zero or more key-value entries depending on whether it's unset, set, or a repeated field.  The bulk of the field data will be represented in the key and the keys will be encoded as [orderedcodes](https://godoc.org/github.com/google/orderedcode).  It's important that each key be unique to fit storage models like LevelDB which are not multimaps (unlike the underlying SSTables).  Tags within keys will indicate how to decode the values themselves and help order the key-value entries.  The ordering of the keys will be chosen as to best fit a performant server, allowing for easy, single-pass scans to retrieve all possible data.\n+\n+## FileDecorations\n+\n+### Existing ProtocolBuffer definition:\n+\n+```protobuf\n+// Simplified FileDecorations\n+message FileDecorations {\n+  File file = 1;\n+\n+  repeated Decoration decoration = 2;\n+  repeated ExpandedAnchor target_definitions = 3;\n+  repeated Node target = 4;\n+  repeated Override target_override = 5;\n+  repeated kythe.proto.common.Diagnostic diagnostic = 6;\n+\n+  message Decoration {\n+    RawAnchor anchor = 1;\n+    string kind = 2;\n+    string target = 5;\n+    string target_definition = 4;\n+  }\n+  message Override {\n+    enum Kind {\n+      OVERRIDES  = 0;\n+      EXTENDS    = 1;\n+    }\n+    string overriding = 1;\n+    string overridden = 2;\n+    string overridden_definition = 5;\n+    Kind kind = 3;\n+    kythe.proto.common.MarkedSource marked_source = 4;\n+  }\n+}\n+```\n+\n+### Key-value format:\n+\n+| key | value | kind |\n+|---|---|---|\n+| `\"fd\"-file` | `FileDecorationsIndex` | file metadata |\n+| `\"fd\"-file-00-start-end` | bytes | file contents for span |\n+| `\"fd\"-file-10-start-end-kind-target` | `<empty>` | decoration targets |\n+| `\"fd\"-file-20-target-kind-override` | `<empty>` | override per target |\n+| `\"fd\"-file-30-target` | `srvpb.Node` | target node facts |\n+| `\"fd\"-file-40-target` | `spb.VName` | definition for each target |\n+| `\"fd\"-file-50-def` | `srvpb.ExpandedAnchor` | each definition's location |\n+| `\"fd\"-file-60-override` | `MarkedSource` | `MarkedSource` per override |\n+| `\"fd\"-file-70-start-end-hash` | `Diagnostic` | `Diagnostic` per span (use `-1:-1` span for entire file) |\n+\n+\n+Notes:\n+\n+* Each value will be contained within a wrapper message to support evolution of the table format\n+* All file decoration entries are prefixed by \"fd\" and the file's `VName`\n+* `VNames` will be stored instead of [Kythe tickets](https://kythe.io/docs/kythe-uri-spec.html)\n+* The `FileDecorationsIndex` is used to store metadata for the rest of the entries and acts as an existence check\n+* File text is chunked to support very large files (most files should be a single entry)\n+* Decorations are ordered by span to support a narrow `FileDecorationsRequest` (group 10)\n+* Node facts/definitions (groups 30 and 40) are stored after decorations and overrides to allow for the reader to skip over unneeded nodes (due to a narrow request)\n+* Each definition is stored uniquely (group 50) and are related to nodes separately (group 40)\n+\n+## CrossReferences\n+\n+### Existing ProtocolBuffer definition:\n+\n+```protobuf\n+// Simplified PagedCrossReference (no paging, removed deprecated features)\n+message PagedCrossReferences {\n+  string source_ticket = 1;\n+  kythe.proto.common.MarkedSource marked_source = 6;\n+  repeated string merge_with = 7;\n+  \n+  repeated Group group = 2;\n+  Node source_node = 8;\n+  \n+  message Group {\n+    string kind = 1;\n+    repeated ExpandedAnchor anchor = 2;\n+    repeated RelatedNode related_node = 3;\n+    repeated Caller caller = 4;\n+  }\n+  message RelatedNode {\n+    Node node = 1;\n+    int32 ordinal = 2;\n+  }\n+  message Caller {\n+    ExpandedAnchor caller = 1;\n+    string semantic_caller = 2;\n+    kythe.proto.common.MarkedSource marked_source = 3;\n+    repeated ExpandedAnchor callsite = 4;\n+  }\n+}\n+```\n+\n+### Key-value format:\n+\n+| key | value | kind |\n+|---|---|---|\n+| `\"xr\"-source` | `CrossReferencesIndex` |  |\n+| `\"xr\"-source-00-kind-file-start-end` | `srvpb.ExpandedAnchor` | Regular cross-reference |\n+| `\"xr\"-source-10-kind-ordinal-node` | `srvpb.Node` | Related nodes |\n+| `\"xr\"-source-20-caller` | `srvpb.ExpandedAnchor`+`MarkedSource` | Definition for Caller |\n+| `\"xr\"-source-20-caller-file-start-end` | `srvpb.ExpandedAnchor` | Callsite |\n+\n+\n+Notes:\n+\n+* Each value will be contained within a wrapper message to support evolution of the table format\n+* All cross-reference entries are prefixed by \"xr\" and the node's `VName`\n+* `VNames` will be stored instead of [Kythe tickets](https://kythe.io/docs/kythe-uri-spec.html)\n+* The `CrossReferencesIndex` is used to store metadata for the rest of the entries (e.g. counts, node facts, `merge_with`) and acts as an existence check\n+* `Group`s are not used (each `Group` message is spread across multiple KV entries with a shared prefix)\n+* `kind` fields in keys include a \"priority\" to achieve desired sorting order (this could alternatively be included in the group number)\n+* After their `kind`, cross-references are ordered by file location\n+\n+## Beam Pipeline","path":"kythe/docs/rfc/2909.md","position":138,"original_position":138,"commit_id":"4fdc859594734805ada172170809a707a7ac3fe4","original_commit_id":"4fdc859594734805ada172170809a707a7ac3fe4","user":{"login":"creachadair","id":19258,"node_id":"MDQ6VXNlcjE5MjU4","avatar_url":"https://avatars1.githubusercontent.com/u/19258?v=4","gravatar_id":"","url":"https://api.github.com/users/creachadair","html_url":"https://github.com/creachadair","followers_url":"https://api.github.com/users/creachadair/followers","following_url":"https://api.github.com/users/creachadair/following{/other_user}","gists_url":"https://api.github.com/users/creachadair/gists{/gist_id}","starred_url":"https://api.github.com/users/creachadair/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/creachadair/subscriptions","organizations_url":"https://api.github.com/users/creachadair/orgs","repos_url":"https://api.github.com/users/creachadair/repos","events_url":"https://api.github.com/users/creachadair/events{/privacy}","received_events_url":"https://api.github.com/users/creachadair/received_events","type":"User","site_admin":false},"body":"Is there a link to docs for this, that we could add here?","created_at":"2018-08-08T22:54:53Z","updated_at":"2018-08-08T22:55:37Z","html_url":"https://github.com/google/kythe/pull/2909#discussion_r208761127","pull_request_url":"https://api.github.com/repos/google/kythe/pulls/2909","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/google/kythe/pulls/comments/208761127"},"html":{"href":"https://github.com/google/kythe/pull/2909#discussion_r208761127"},"pull_request":{"href":"https://api.github.com/repos/google/kythe/pulls/2909"}}},"pull_request":{"url":"https://api.github.com/repos/google/kythe/pulls/2909","id":207156009,"node_id":"MDExOlB1bGxSZXF1ZXN0MjA3MTU2MDA5","html_url":"https://github.com/google/kythe/pull/2909","diff_url":"https://github.com/google/kythe/pull/2909.diff","patch_url":"https://github.com/google/kythe/pull/2909.patch","issue_url":"https://api.github.com/repos/google/kythe/issues/2909","number":2909,"state":"open","locked":false,"title":"RFC: Kythe Columnar Serving Format","user":{"login":"schroederc","id":8930878,"node_id":"MDQ6VXNlcjg5MzA4Nzg=","avatar_url":"https://avatars3.githubusercontent.com/u/8930878?v=4","gravatar_id":"","url":"https://api.github.com/users/schroederc","html_url":"https://github.com/schroederc","followers_url":"https://api.github.com/users/schroederc/followers","following_url":"https://api.github.com/users/schroederc/following{/other_user}","gists_url":"https://api.github.com/users/schroederc/gists{/gist_id}","starred_url":"https://api.github.com/users/schroederc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/schroederc/subscriptions","organizations_url":"https://api.github.com/users/schroederc/orgs","repos_url":"https://api.github.com/users/schroederc/repos","events_url":"https://api.github.com/users/schroederc/events{/privacy}","received_events_url":"https://api.github.com/users/schroederc/received_events","type":"User","site_admin":false},"body":"","created_at":"2018-08-08T22:31:12Z","updated_at":"2018-08-08T22:55:37Z","closed_at":null,"merged_at":null,"merge_commit_sha":"c6167863f6797f59281de44c8f159c173afd3b2c","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":1018847185,"node_id":"MDU6TGFiZWwxMDE4ODQ3MTg1","url":"https://api.github.com/repos/google/kythe/labels/RFC","name":"RFC","color":"bc057c","default":false},{"id":951875280,"node_id":"MDU6TGFiZWw5NTE4NzUyODA=","url":"https://api.github.com/repos/google/kythe/labels/post-processing","name":"post-processing","color":"d93f0b","default":false},{"id":932818471,"node_id":"MDU6TGFiZWw5MzI4MTg0NzE=","url":"https://api.github.com/repos/google/kythe/labels/storage","name":"storage","color":"d4c5f9","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/google/kythe/pulls/2909/commits","review_comments_url":"https://api.github.com/repos/google/kythe/pulls/2909/comments","review_comment_url":"https://api.github.com/repos/google/kythe/pulls/comments{/number}","comments_url":"https://api.github.com/repos/google/kythe/issues/2909/comments","statuses_url":"https://api.github.com/repos/google/kythe/statuses/4fdc859594734805ada172170809a707a7ac3fe4","head":{"label":"schroederc:columnar-format","ref":"columnar-format","sha":"4fdc859594734805ada172170809a707a7ac3fe4","user":{"login":"schroederc","id":8930878,"node_id":"MDQ6VXNlcjg5MzA4Nzg=","avatar_url":"https://avatars3.githubusercontent.com/u/8930878?v=4","gravatar_id":"","url":"https://api.github.com/users/schroederc","html_url":"https://github.com/schroederc","followers_url":"https://api.github.com/users/schroederc/followers","following_url":"https://api.github.com/users/schroederc/following{/other_user}","gists_url":"https://api.github.com/users/schroederc/gists{/gist_id}","starred_url":"https://api.github.com/users/schroederc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/schroederc/subscriptions","organizations_url":"https://api.github.com/users/schroederc/orgs","repos_url":"https://api.github.com/users/schroederc/repos","events_url":"https://api.github.com/users/schroederc/events{/privacy}","received_events_url":"https://api.github.com/users/schroederc/received_events","type":"User","site_admin":false},"repo":{"id":109754450,"node_id":"MDEwOlJlcG9zaXRvcnkxMDk3NTQ0NTA=","name":"kythe","full_name":"schroederc/kythe","owner":{"login":"schroederc","id":8930878,"node_id":"MDQ6VXNlcjg5MzA4Nzg=","avatar_url":"https://avatars3.githubusercontent.com/u/8930878?v=4","gravatar_id":"","url":"https://api.github.com/users/schroederc","html_url":"https://github.com/schroederc","followers_url":"https://api.github.com/users/schroederc/followers","following_url":"https://api.github.com/users/schroederc/following{/other_user}","gists_url":"https://api.github.com/users/schroederc/gists{/gist_id}","starred_url":"https://api.github.com/users/schroederc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/schroederc/subscriptions","organizations_url":"https://api.github.com/users/schroederc/orgs","repos_url":"https://api.github.com/users/schroederc/repos","events_url":"https://api.github.com/users/schroederc/events{/privacy}","received_events_url":"https://api.github.com/users/schroederc/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/schroederc/kythe","description":"Kythe is a pluggable, (mostly) language-agnostic ecosystem for building tools that work with code.","fork":true,"url":"https://api.github.com/repos/schroederc/kythe","forks_url":"https://api.github.com/repos/schroederc/kythe/forks","keys_url":"https://api.github.com/repos/schroederc/kythe/keys{/key_id}","collaborators_url":"https://api.github.com/repos/schroederc/kythe/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/schroederc/kythe/teams","hooks_url":"https://api.github.com/repos/schroederc/kythe/hooks","issue_events_url":"https://api.github.com/repos/schroederc/kythe/issues/events{/number}","events_url":"https://api.github.com/repos/schroederc/kythe/events","assignees_url":"https://api.github.com/repos/schroederc/kythe/assignees{/user}","branches_url":"https://api.github.com/repos/schroederc/kythe/branches{/branch}","tags_url":"https://api.github.com/repos/schroederc/kythe/tags","blobs_url":"https://api.github.com/repos/schroederc/kythe/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/schroederc/kythe/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/schroederc/kythe/git/refs{/sha}","trees_url":"https://api.github.com/repos/schroederc/kythe/git/trees{/sha}","statuses_url":"https://api.github.com/repos/schroederc/kythe/statuses/{sha}","languages_url":"https://api.github.com/repos/schroederc/kythe/languages","stargazers_url":"https://api.github.com/repos/schroederc/kythe/stargazers","contributors_url":"https://api.github.com/repos/schroederc/kythe/contributors","subscribers_url":"https://api.github.com/repos/schroederc/kythe/subscribers","subscription_url":"https://api.github.com/repos/schroederc/kythe/subscription","commits_url":"https://api.github.com/repos/schroederc/kythe/commits{/sha}","git_commits_url":"https://api.github.com/repos/schroederc/kythe/git/commits{/sha}","comments_url":"https://api.github.com/repos/schroederc/kythe/comments{/number}","issue_comment_url":"https://api.github.com/repos/schroederc/kythe/issues/comments{/number}","contents_url":"https://api.github.com/repos/schroederc/kythe/contents/{+path}","compare_url":"https://api.github.com/repos/schroederc/kythe/compare/{base}...{head}","merges_url":"https://api.github.com/repos/schroederc/kythe/merges","archive_url":"https://api.github.com/repos/schroederc/kythe/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/schroederc/kythe/downloads","issues_url":"https://api.github.com/repos/schroederc/kythe/issues{/number}","pulls_url":"https://api.github.com/repos/schroederc/kythe/pulls{/number}","milestones_url":"https://api.github.com/repos/schroederc/kythe/milestones{/number}","notifications_url":"https://api.github.com/repos/schroederc/kythe/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/schroederc/kythe/labels{/name}","releases_url":"https://api.github.com/repos/schroederc/kythe/releases{/id}","deployments_url":"https://api.github.com/repos/schroederc/kythe/deployments","created_at":"2017-11-06T21:55:11Z","updated_at":"2018-07-18T16:55:49Z","pushed_at":"2018-08-08T22:31:22Z","git_url":"git://github.com/schroederc/kythe.git","ssh_url":"git@github.com:schroederc/kythe.git","clone_url":"https://github.com/schroederc/kythe.git","svn_url":"https://github.com/schroederc/kythe","homepage":"https://kythe.io","size":156772,"stargazers_count":0,"watchers_count":0,"language":"C++","has_issues":true,"has_projects":true,"has_downloads":false,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"google:master","ref":"master","sha":"273f3a2c8fb5e9ba026ed738b49503b18da3d29f","user":{"login":"google","id":1342004,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzNDIwMDQ=","avatar_url":"https://avatars1.githubusercontent.com/u/1342004?v=4","gravatar_id":"","url":"https://api.github.com/users/google","html_url":"https://github.com/google","followers_url":"https://api.github.com/users/google/followers","following_url":"https://api.github.com/users/google/following{/other_user}","gists_url":"https://api.github.com/users/google/gists{/gist_id}","starred_url":"https://api.github.com/users/google/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/google/subscriptions","organizations_url":"https://api.github.com/users/google/orgs","repos_url":"https://api.github.com/users/google/repos","events_url":"https://api.github.com/users/google/events{/privacy}","received_events_url":"https://api.github.com/users/google/received_events","type":"Organization","site_admin":false},"repo":{"id":29174627,"node_id":"MDEwOlJlcG9zaXRvcnkyOTE3NDYyNw==","name":"kythe","full_name":"google/kythe","owner":{"login":"google","id":1342004,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzNDIwMDQ=","avatar_url":"https://avatars1.githubusercontent.com/u/1342004?v=4","gravatar_id":"","url":"https://api.github.com/users/google","html_url":"https://github.com/google","followers_url":"https://api.github.com/users/google/followers","following_url":"https://api.github.com/users/google/following{/other_user}","gists_url":"https://api.github.com/users/google/gists{/gist_id}","starred_url":"https://api.github.com/users/google/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/google/subscriptions","organizations_url":"https://api.github.com/users/google/orgs","repos_url":"https://api.github.com/users/google/repos","events_url":"https://api.github.com/users/google/events{/privacy}","received_events_url":"https://api.github.com/users/google/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/google/kythe","description":"Kythe is a pluggable, (mostly) language-agnostic ecosystem for building tools that work with code.","fork":false,"url":"https://api.github.com/repos/google/kythe","forks_url":"https://api.github.com/repos/google/kythe/forks","keys_url":"https://api.github.com/repos/google/kythe/keys{/key_id}","collaborators_url":"https://api.github.com/repos/google/kythe/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/google/kythe/teams","hooks_url":"https://api.github.com/repos/google/kythe/hooks","issue_events_url":"https://api.github.com/repos/google/kythe/issues/events{/number}","events_url":"https://api.github.com/repos/google/kythe/events","assignees_url":"https://api.github.com/repos/google/kythe/assignees{/user}","branches_url":"https://api.github.com/repos/google/kythe/branches{/branch}","tags_url":"https://api.github.com/repos/google/kythe/tags","blobs_url":"https://api.github.com/repos/google/kythe/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/google/kythe/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/google/kythe/git/refs{/sha}","trees_url":"https://api.github.com/repos/google/kythe/git/trees{/sha}","statuses_url":"https://api.github.com/repos/google/kythe/statuses/{sha}","languages_url":"https://api.github.com/repos/google/kythe/languages","stargazers_url":"https://api.github.com/repos/google/kythe/stargazers","contributors_url":"https://api.github.com/repos/google/kythe/contributors","subscribers_url":"https://api.github.com/repos/google/kythe/subscribers","subscription_url":"https://api.github.com/repos/google/kythe/subscription","commits_url":"https://api.github.com/repos/google/kythe/commits{/sha}","git_commits_url":"https://api.github.com/repos/google/kythe/git/commits{/sha}","comments_url":"https://api.github.com/repos/google/kythe/comments{/number}","issue_comment_url":"https://api.github.com/repos/google/kythe/issues/comments{/number}","contents_url":"https://api.github.com/repos/google/kythe/contents/{+path}","compare_url":"https://api.github.com/repos/google/kythe/compare/{base}...{head}","merges_url":"https://api.github.com/repos/google/kythe/merges","archive_url":"https://api.github.com/repos/google/kythe/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/google/kythe/downloads","issues_url":"https://api.github.com/repos/google/kythe/issues{/number}","pulls_url":"https://api.github.com/repos/google/kythe/pulls{/number}","milestones_url":"https://api.github.com/repos/google/kythe/milestones{/number}","notifications_url":"https://api.github.com/repos/google/kythe/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/google/kythe/labels{/name}","releases_url":"https://api.github.com/repos/google/kythe/releases{/id}","deployments_url":"https://api.github.com/repos/google/kythe/deployments","created_at":"2015-01-13T05:39:06Z","updated_at":"2018-08-08T18:37:59Z","pushed_at":"2018-08-08T22:31:23Z","git_url":"git://github.com/google/kythe.git","ssh_url":"git@github.com:google/kythe.git","clone_url":"https://github.com/google/kythe.git","svn_url":"https://github.com/google/kythe","homepage":"https://kythe.io","size":162343,"stargazers_count":1024,"watchers_count":1024,"language":"C++","has_issues":true,"has_projects":false,"has_downloads":false,"has_wiki":false,"has_pages":false,"forks_count":128,"mirror_url":null,"archived":false,"open_issues_count":155,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":128,"open_issues":155,"watchers":1024,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/google/kythe/pulls/2909"},"html":{"href":"https://github.com/google/kythe/pull/2909"},"issue":{"href":"https://api.github.com/repos/google/kythe/issues/2909"},"comments":{"href":"https://api.github.com/repos/google/kythe/issues/2909/comments"},"review_comments":{"href":"https://api.github.com/repos/google/kythe/pulls/2909/comments"},"review_comment":{"href":"https://api.github.com/repos/google/kythe/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/google/kythe/pulls/2909/commits"},"statuses":{"href":"https://api.github.com/repos/google/kythe/statuses/4fdc859594734805ada172170809a707a7ac3fe4"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"creachadair"}}