大規模ソフトウェア(Chromium)を手探る user profileに設定を追加する
前回までのブログで任意のキーワードを含む検索結果に基づく自動補完を除くことができるようになりました。
今回はChromeのUI側で変更されたキーワードをuser profileで保存し、C++側のOmniboxの自動補完を行なっているクラスでこのデータを呼べるように変更します。
昨年(2016年度)の「大規模ソフトウェアを手探る」でChromiumを題材にしていた方がuser profileへの追加をした際のステップを丁寧に説明してくださっており、実装の際にとても参考になりました。
Preferenceに表示したくないキーワードのデータを追加する
実際にキーワードフィルターをかける場合にはChromeの設定画面でユーザーのよって登録されたキーワードのデータを取ってきてフィルターをかける必要があります。ユーザーの登録したChromiumの設定はUser Profileに保存され、それぞれに固有のkey(pref_name)によって識別されます。
preferecesに任意の設定について追加したい場合には、
- PrefNamesクラスで新しく設定したいpreferenceのnameとデフォルト値を登録する。
- ProfileImplクラスのRegisterProfilePrefsd()という関数でこのpreferenceを登録する。
というステップでProfileにこの新しいpreferenceの値がストアされるようになり、PrefServiceを呼ぶことで、C++のプログラムから参照できるようになります。
詳しい説明は公式を参照してください。
まずchrome/common/pref_names.h
に追加したいpreferenceの項目を追加します。今回はとりあえず全てのpreferenceの最後に新しくkRestrictedKeywordという項目を追加しました。
namespace prefs { // Profile prefs. Please add Local State prefs below instead. extern const char kChildAccountStatusKnown[]; ... // 変更ここから extern const char kRestrictedKeyword[]; // 変更ここまで } // namespace prefs
これで宣言はできたので、cpp側でUI(JavaScript)側からこのpreferenceを参照する時に必要なpref_nameを登録します。ここに格納される値はデフォルト値ではなくてキーとして使われる識別子であることに注意してください。1
namespace prefs { // *************** PROFILE PREFS *************** // These are attached to the user profile ... // 変更ここから const char kRestrictedKeyword[] = "RestrictedKeyword"; // 変更ここまで } // namespace prefs
上のドキュメントにも書いてある通り、 pref_names.{h,cc}で定義された新たなpreferenceはchrome /browser/profiles/profile_impl.cc
で登録される必要があります。
// RegisterProfilePrefsd()というvoid関数でpreferenceの登録が行われている void ProfileImpl::RegisterProfilePrefs( user_prefs::PrefRegistrySyncable* registry) { registry->RegisterBooleanPref(prefs::kSavingBrowserHistoryDisabled, false); ... // 変更ここから registry->RegisterStringPref(prefs::kRestrictedKeyword, std::string()); // 変更ここまで }
デフォルトの値を設定したい場合にはRegisterStringPref()
の二つ目の引数で設定ができ、例えばregistry->RegisterStringPref(prefs::kRestrictedKeyword, "foobarhoge");
とすればユーザーによって制限したキーワードが設定されるまで、"foobarhoge"がkRestrictedKeywordのデフォルト値として、foobarhogeを含む検索結果が表示されないようになります。
これでuser profileに新しいpreferenceを追加することができたので、いよいよC++側からpreferenceの情報を取得して自動補完候補にフィルターをかける機能を実装しようと思います。
AutocompleteControllerからUser profileを参照する
ユーザーのpreferencesを取得するためには、PrefServiceをservice clientから呼ぶ必要があります。 前回のブログの実装では実質的に自動補完の候補をソートし、重複を除く関数であるSortAndDedup()というAutocompleteResultクラスの関数の中でフィルター機能を実装しました。
ただこの実装方法を用いると、AutocompleteResultクラスがservice clientに順ずるものにアクセスできないため、この関数を呼び出しているAutocompleteController側から何らかの方法でkRestrictedKeywordの情報を渡す必要があります。 AutocompleteControllerはSortAndCull()という関数を経由して間接的にSortAndDedup()関数を読んでいます。
SortAndCull()関数がconst string kRestrictedKeyword
という引数を追加で取るように変更することも考えたのですが、Call Hierarchyをチェックしたところ 15 occurrencesだった(つまり15箇所で呼ばれている)ため、
全部変更するのちょっとめんどくさいな...ということで、新たにSortAndCullWithKeyword()
という関数をAutocompleteResultクラスに追加し、keywordが存在する場合にはこちらの関数が呼ばれるように変更しました。
まず、関数の宣言をヘッダーファイルに追加します。
void SortAndCull(const AutocompleteInput& input, TemplateURLService* template_url_service); // 変更ここから void SortAndCullWithKeyword(const AutocompleteInput& input, TemplateURLService* template_url_service, const std::string& keyword); // 変更ここまで
次に、新たに追加したSortAndCullWithKeyword()をcppファイルで定義します。基本的にはオリジナルのSortAndCull()関数を内部で呼びつつ、 keywordに従ってフィルターをかけています。フィルターをかけるに当たって、入力文字列とマッチした自動補完候補のベクターであるmatchesの先頭がdefault_matchとして保持されていなくてはいけないので、最後にdefault_matchを設定し直しています。
// SortAndCull()関数の次にSortAndCullWithKeyword()関数を追加する。 // 変更ここから void AutocompleteResult::SortAndCullWithKeyword( const AutocompleteInput& input, TemplateURLService* template_url_service, const std::string& keyword) { // デフォルトのSortAndCullを呼ぶ SortAndCull(input, template_url_service); // キーワードフィルターをかける for (auto itr = matches_.begin(); itr != matches_.end();) { bool be_removed = itr->type == AutocompleteMatchType::HISTORY_TITLE || itr->type == AutocompleteMatchType::HISTORY_BODY || itr->type == AutocompleteMatchType::SEARCH_HISTORY || itr->type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL; if (be_removed && base::EqualsASCII(itr->contents, keyword) == true) { itr = matches_.erase(itr); } else { itr++; } } // default_match_を設定し直す default_match_ = matches_.begin(); } // 変更ここまで
最後に、AutocompleteController側からSortAndCullWIth()の代わりにSortAndCullWithKeyword()を呼び、preferencesにストアされているkRestrictedKeywordを渡すように書き換えます。
void AutocompleteController::UpdateResult( bool regenerate_result, bool force_notify_default_match_changed) { TRACE_EVENT0("omnibox", "AutocompleteController::UpdateResult"); const bool last_default_was_valid = result_.default_match() != result_.end(); // The following three variables are only set and used if // |last_default_was_valid|. base::string16 last_default_fill_into_edit, last_default_keyword, last_default_associated_keyword; if (last_default_was_valid) { last_default_fill_into_edit = result_.default_match()->fill_into_edit; last_default_keyword = result_.default_match()->keyword; if (result_.default_match()->associated_keyword) { last_default_associated_keyword = result_.default_match()->associated_keyword->keyword; } } if (regenerate_result) result_.Reset(); AutocompleteResult last_result; last_result.Swap(&result_); for (Providers::const_iterator i(providers_.begin()); i != providers_.end(); ++i) result_.AppendMatches(input_, (*i)->matches()); // 変更ここから PrefService* prefs = provider_client_->GetPrefs(); const std::string keyword = prefs->GetString(prefs::kRestrictedKeyword); // 非表示キーワードが設定されていた時はSortAndCullWithKeyword()を、 // そうでない時は通常のSortAndCull()を呼ぶ。 if (keyword) { result_.SortAndCullWithKeyword(input_, template_url_service_, keyword); } else { result_.SortAndCull(input_, template_url_service_); } // 変更ここまで
これでuser profile preferenceからユーザが設定したキーワードの情報を読み込み、Omniboxの自動補完候補に出したくないものを単語でフィルタリングできるようになりました。やったね。
ただこれだと、単一のキーワードに対してのみしかフィルタリング機能を使えないことになります。やっぱり複数のキーワードに対してフィルタリング機能を使いたいよね、ということになったので、キーワードをカンマで区切られた文字列で渡してもらい、それをtokenizeして複数のキーワードに対してフィルター機能を実装をするようにしました。
// 変更ここから // tokenizeを行う関数を追加する std::vector<std::string> TokenizesKeywordsStringToKeywordsVec( const std::string& keyword) { std::vector<std::string> tokens; std::size_t start = 0, end = 0; while ((end = keyword.find(',', start)) != std::string::npos) { tokens.push_back(keyword.substr(start, end - start)); start = end + 1; } tokens.push_back(keyword.substr(start)); return tokens; } void AutocompleteResult::SortAndCullWithKeyword( const AutocompleteInput& input, TemplateURLService* template_url_service, const std::string& keyword) { SortAndCull(input, template_url_service); std::vector<std::string> tokens = TokenizesKeywordsStringToKeywordsVec(keyword); for (auto i = tokens.begin(); i != tokens.end(); i++) { LOG(INFO) << *i << ' '; for (auto itr = matches_.begin(); itr != matches_.end();) { bool be_removed = itr->type == AutocompleteMatchType::HISTORY_TITLE || itr->type == AutocompleteMatchType::HISTORY_BODY || itr->type == AutocompleteMatchType::SEARCH_HISTORY || itr->type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL; if (be_removed && base::EqualsASCII(itr->contents, *i) == true) { itr = matches_.erase(itr); } else { itr++; } } } default_match_ = matches_.begin(); } // 変更ここまで
また、この実装だとまだ「完全に一致キーワードと一致する検索履歴」しかフィルターがかけられず、また特に英語の場合は大文字小文字の区別がされてしまって 結果的にフィルターがうまくかからないケースもありえます。 そこで最後に以下のように変更を加えました。 またついでにmatches_から消去して良いか、matchTypeから判別する部分についても関数として切り出し、Tokenizeを行う関数、全てのアルファベットの入力文字列を小文字化する関数とともに無名名前空間に入れます。
// 変更ここまで namespace { base::string16 NormalizeRestrictedKeyword(std::string s) { return base::ToLowerASCII(base::UTF8ToUTF16(std::move(s))); } std::vector<base::string16> TokenizesKeywordsStringToKeywordsVec( const std::string& keyword) { std::vector<std::string> TokenizesKeywordsStringToKeywordsVec( const std::string& keyword) { std::vector<base::string16> tokens; std::size_t start = 0, end; while ((end = keyword.find(',', start)) != std::string::npos) { std::vector<base::string16> tokens; std::size_t start = 0, end; while ((end = keyword.find(',', start)) != std::string::npos) { tokens.push_back( NormalizeRestrictedKeyword(keyword.substr(start, end - start))); start = end + 1; } tokens.push_back(NormalizeRestrictedKeyword(keyword.substr(start))); return tokens; } bool IsRemovableTypeFromMatch(AutocompleteMatchType::Type type) { return type == AutocompleteMatchType::HISTORY_TITLE || type == AutocompleteMatchType::HISTORY_BODY || type == AutocompleteMatchType::SEARCH_HISTORY || type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL; } } // namespace void AutocompleteResult::SortAndCullWithKeyword( const AutocompleteInput& input, TemplateURLService* template_url_service, const std::string& keyword) { std::vector<base::string16> restricted_keywords = TokenizesKeywordsStringToKeywordsVec(keyword); matches_.erase( std::remove_if( matches_.begin(), matches_.end(), [&restricted_keywords](const AutocompleteMatch& match) { bool removable = IsRemovableTypeFromMatch(match.type); if (removable) { base::string16 match_text = base::ToLowerASCII(match.contents); for (auto& restricted_keyword : restricted_keywords) { if (match_text.find(restricted_keyword) != base::string16::npos) { return true; } } } return false; }), matches_.end()); SortAndCull(input, template_url_service); } // 変更ここまで
ただこの変更を加えて実行してみると、キーワードフィルタリング自体はできるのですが、なぜか「キーワードと入力文字列が完全一致した時にcrashする」という現象が発生するようになります…なぜだ… まずは王道、crashした時のback traceをみてみます。
Check failed: input.text().empty() != default_match_-> allowed_to_be_default_match (0 vs. 0)fill_into_edit=ポケモンgo, provider=Search, input=pokemongo #0 0x7f6fac40677d base::debug::StackTrace::StackTrace() #1 0x7f6fac404b4c base::debug::StackTrace::StackTrace() #2 0x7f6fac4962ba logging::LogMessage::~LogMessage() #3 0x558c51544aee AutocompleteResult::SortAndCull() #4 0x558c51540e13 AutocompleteResult::CopyOldMatches() #5 0x558c538379e0 AutocompleteController::UpdateResult() #6 0x558c53836882 AutocompleteController::Start()
この時はOmniboxの入力している最中に落ちているのでinput.text().empty() == false
となっています。
そのため、本来ならばdefault_match_-> allowed_to_be_default_match == true
にならなくてはいのですが
default_match_->allowed_to_be_default_match == false
になっているためにDCHECK()で落ちているようです。
この謎のdefault_match_
とallowed_to_be_default_match
について、少し調べてみます。
default_match_とallowed_to_be_default_matchについて調べる
default_match
について、AutocompleteResultクラスには特にコメント等で詳細な記述がないため、
default_match
はAutocompleteMatchというクラスのインスタンスになるため、このクラスを定義するautocomplete_match.cpp
ファイルでallowed_to_be_default_match
についての
めぼしい記述がないか探してみます。
するとallowed_to_be_default_match
について以下のような記述を発見しました。
// If false, the omnibox should prevent this match from being the // default match. Providers should set this to true only if the // user's input, plus any inline autocompletion on this match, would // lead the user to expect a navigation to this match's destination. // For example, with input "foo", a search for "bar" or navigation // to "bar.com" should not set this flag; a navigation to "foo.com" // should only set this flag if ".com" will be inline autocompleted; // and a navigation to "foo/" (an intranet host) or search for "foo" // should set this flag. bool allowed_to_be_default_match;
allowed_to_be_default_match
は様々なProviderから返される自動補完候補に対して、それがdefault_match
になりうるかなり得ないかを表しているようです。
またallowed_to_be_default_match == true
になるのは、ユーザーの入力そのもの、もしくは入力に対しインライン自動補完を加えた候補に対してのみらしいですね。
そのためfoo
とユーザーが入力する時にbar
やbar.com
はdefault_match
にはなり得ないものの、
foo.com
やfoo
はdefault_match
となり得るようです。
つまり、先ほどのバグの原因は
「入力文字列と表示されたくないキーワードが完全に一致するもしくは入力文字列がキーワードを含む時、ユーザーの入力をそのまま候補としてサジェストするmatch(基本的にはこれが一番優先度の高いautocomplete_match
としてdefault_match_
にセットされている)がキーワードフィルタリングによって除かれる。その結果、優先度が二番目以降の候補がSortAndCull()が呼ばれた後default_match_
にセットされるが、優先度が二番目以降の候補はallowed_to_be_default_match == true
となる条件を満たしていないことが多いため2、クラッシュする」
ことだったようです。
元々「Search For...」と呼ばれる、ユーザーの文字列をそのまま検索候補として表示するものについてはbool IsRemovableTypeFromMatch(AutocompleteMatchType::Type type)
関数でチェックをかけ、除去されないようにしていたはずでした。
しかし、おそらく同じキーワードの候補重複を排除する際に、他のProviderから返される別の候補で上書きされた結果除去されてしまったようです。
そこで入力文字列と一致する自動補完候補についてはmatches_から除去しないよう、以下のようにコードを修正しました。
// 変更ここから namespace { base::string16 NormalizeRestrictedKeyword(std::string s) { return base::ToLowerASCII(base::UTF8ToUTF16(std::move(s))); } std::vector<base::string16> TokenizesKeywordsStringToKeywordsVec( const std::string& keyword) { std::vector<base::string16> tokens; std::size_t start = 0, end; while ((end = keyword.find(',', start)) != std::string::npos) { std::vector<base::string16> tokens; std::size_t start = 0, end; while ((end = keyword.find(',', start)) != std::string::npos) { tokens.push_back( NormalizeRestrictedKeyword(keyword.substr(start, end - start))); start = end + 1; } tokens.push_back(NormalizeRestrictedKeyword(keyword.substr(start))); return tokens; } bool IsRemovableTypeFromMatch(AutocompleteMatchType::Type type) { return type == AutocompleteMatchType::HISTORY_TITLE || type == AutocompleteMatchType::HISTORY_BODY || type == AutocompleteMatchType::SEARCH_HISTORY || type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL; } } // namespace void AutocompleteResult::SortAndCullWithKeyword( const AutocompleteInput& input, TemplateURLService* template_url_service, const std::string& keyword) { std::vector<base::string16> restricted_keywords = TokenizesKeywordsStringToKeywordsVec(keyword); const base::string16& input_text = input.text(); // ラムダ式にOmniboxの入力ボックス内の文字列 input_textも一緒に渡す matches_.erase( std::remove_if( matches_.begin(), matches_.end(), [&restricted_keywords, &input_text](const AutocompleteMatch& match) { bool removable = IsRemovableTypeFromMatch(match.type); if (removable) { base::string16 match_text = base::ToLowerASCII(match.contents); for (auto& restricted_keyword : restricted_keywords) { // Omniboxの入力文字列と一致しないかつ表示したくないキーワードを含む検索候補を除くように書き換える。 if (input_text != match_text && match_text.find(restricted_keyword) != base::string16::npos) { return true; } } } return false; }), matches_.end()); SortAndCull(input, template_url_service); } // 変更ここまで
これでユーザーの設定した複数キーワードに対しての検索履歴のフィルタリング機能の実装が完了しました。 あとはこのキーワードを設定する設定画面の追加及びJavaScriptの世界とC++の世界の間で情報をやり取りするためのHandlerクラスの追加等を行う必要があります。 これは次回以降のブログでまた説明します。
フィルタリング部分の変更まとめ
これまでに行なって変更を一覧にして載せておきます。
- components/omnibox/browser/autocomplete_result.h
// 変更ここから void SortAndCullWithKeyword(const AutocompleteInput& input, TemplateURLService* template_url_service, const std::string& keyword); // 変更ここまで
- components/omnibox/browser/autocomplete_result.cpp
// 変更ここから namespace { base::string16 NormalizeRestrictedKeyword(std::string s) { return base::ToLowerASCII(base::UTF8ToUTF16(std::move(s))); } std::vector<base::string16> TokenizesKeywordsStringToKeywordsVec( const std::string& keyword) { std::vector<base::string16> tokens; std::size_t start = 0, end; while ((end = keyword.find(',', start)) != std::string::npos) { std::vector<base::string16> tokens; std::size_t start = 0, end; while ((end = keyword.find(',', start)) != std::string::npos) { tokens.push_back( NormalizeRestrictedKeyword(keyword.substr(start, end - start))); start = end + 1; } tokens.push_back(NormalizeRestrictedKeyword(keyword.substr(start))); return tokens; } bool IsRemovableTypeFromMatch(AutocompleteMatchType::Type type) { return type == AutocompleteMatchType::HISTORY_TITLE || type == AutocompleteMatchType::HISTORY_BODY || type == AutocompleteMatchType::SEARCH_HISTORY || type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL; } } // namespace void AutocompleteResult::SortAndCullWithKeyword( const AutocompleteInput& input, TemplateURLService* template_url_service, const std::string& keyword) { std::vector<base::string16> restricted_keywords = TokenizesKeywordsStringToKeywordsVec(keyword); const base::string16& input_text = input.text(); matches_.erase( std::remove_if( matches_.begin(), matches_.end(), [&restricted_keywords, &input_text](const AutocompleteMatch& match) { bool removable = IsRemovableTypeFromMatch(match.type); if (removable) { base::string16 match_text = base::ToLowerASCII(match.contents); for (auto& restricted_keyword : restricted_keywords) { if (input_text != match_text && match_text.find(restricted_keyword) != base::string16::npos) { return true; } } } return false; }), matches_.end()); SortAndCull(input, template_url_service); } // 変更ここまで
- chrome/common/pref_names.h
namespace prefs { ... // 変更ここから extern const char kRestrictedKeyword[]; // 変更ここまで } // namespace prefs
- chrome/common/pref_names.cpp
namespace prefs { ... // 変更ここから const char kRestrictedKeyword[] = "RestrictedKeyword"; // 変更ここまで } // namespace prefs
- chrome/browser/profiles/profile_impl.cc
void ProfileImpl::RegisterProfilePrefs( user_prefs::PrefRegistrySyncable* registry) { ... // 変更ここから registry->RegisterStringPref(prefs::kRestrictedKeyword, std::string()); // 変更ここまで }
大規模ソフトウェアを手探る 検索ボックスにおける自動補完・サジェスチョンのデータの流れを追う
前回のブログでは、過去の検索ワードをDBから取ってきて自動補完を行うクラスに返すSearchProviderクラスを見つけてフィルター機能を実装してみたものの、実際の自動補完候補にフィルターしたはずのワードが残ってしまうと言う謎現象に遭遇しました。 今回ではこの問題を解消し、C++側の実装変更を完了するまでをまとめたいと思います。
データの流れを追う
SearchProviderクラスから流れてくるデータではデータのフィルタリングができているのに、Omniboxには補完候補として表れることから、SearchProvider以外のクラスからこのデータが供給されているのではないかと考え、まず自動補完のデータがどういった流れで受け渡されているのかを調べることにしました。
gdbでブレークポイントを設定してみたり、エラーのバックトレース結果をみたり、地道にコードをサーチしたりして、Omniboxでユーザーが入力するたびに、AutocompleteControllerというクラスが各Providerからデータを集めて自動補完候補autocomplete_matchesを集め、このAutocompleteResultsクラスにこのマッチしたデータを渡すと、このAutocompleteResultsクラスで関連度に基づいてこのmatchesをソートし、重複を排除してControllerに渡し、AutocompleteControllerがこの結果に基づいてOmniboxのサジェスチョンを更新していることがわかりました。 実際には他にもHistoryQuickProviderなどいくつかのproviderからAutocompleteControllerに自動補完候補が提供されているのですが、図では省略しています。
前回の変更ではいくつかのProviderのうち、「検索時のクエリのうち、入力文字列にマッチするものを候補として返す」SearchProviderの結果から禁止したいキーワードとマッチするものを除くようにしていました。 図の中の赤いデータが表示したくないキーワードを含む検索履歴のデータを表しています。SearchProviderでこの特定のキーワードを含む検索履歴を除いても、全てのProviderから供給された結果を集めると、その中に消したい履歴が残り続けています。
一旦、AutocompleteControllerの中で色々なProviderを集めている部分のコードを確認してみます。
AutocompleteController::AutocompleteController(...) { provider_types &= ~OmniboxFieldTrial::GetDisabledProviderTypes(); if (provider_types & AutocompleteProvider::TYPE_BOOKMARK) providers_.push_back(new BookmarkProvider(provider_client_.get())); if (provider_types & AutocompleteProvider::TYPE_BUILTIN) providers_.push_back(new BuiltinProvider(provider_client_.get())); if (provider_types & AutocompleteProvider::TYPE_HISTORY_QUICK) providers_.push_back(new HistoryQuickProvider(provider_client_.get())); if (provider_types & AutocompleteProvider::TYPE_HISTORY_URL) { history_url_provider_ = new HistoryURLProvider(provider_client_.get(), this); providers_.push_back(history_url_provider_); } if (provider_types & AutocompleteProvider::TYPE_KEYWORD) { keyword_provider_ = new KeywordProvider(provider_client_.get(), this); providers_.push_back(keyword_provider_); } if (provider_types & AutocompleteProvider::TYPE_SEARCH) { search_provider_ = new SearchProvider(provider_client_.get(), this); providers_.push_back(search_provider_); } if (provider_types & AutocompleteProvider::TYPE_SHORTCUTS) providers_.push_back(new ShortcutsProvider(provider_client_.get())); if (provider_types & AutocompleteProvider::TYPE_ZERO_SUGGEST) { zero_suggest_provider_ = ZeroSuggestProvider::Create( provider_client_.get(), history_url_provider_, this); if (zero_suggest_provider_) providers_.push_back(zero_suggest_provider_); } ... }
8種類のProviderからデータを集めていることがわかります。
おそらくこのうちのどれかのProviderがキーワードを含む検索履歴をSearchProviderとは別のProviderから供給されているのではないかと仮説を立て、それぞれのProviderについてどんな結果を返しているのか検証をしてみました。
Chrome Browserのいわゆるprintfデバッグをする際には、個人的にLOG(INFO) <<
を使うことがおすすめです。
使用の仕方はC++のstd::cout
とだいたい同じです。
真犯人が判明
N時間に渡るデバッグの結果、フィルターをかけたはずの検索ワードを返しているProviderが判明しました。 真犯人は ShortcutProvider でした。
工工工エエエエエエェェェェェェ(゚Д゚)ェェェェェェエエエエエエ工工工
なんと一番無害そうというか関係なさそうなProviderが、実は禁止キーワード文字列をAutocompleteProviderに返していました…... ShortcutProviderクラスのコードをみてみると、
// Provider of recently autocompleted links. Provides autocomplete suggestions // from previously selected suggestions. The more often a user selects a // suggestion for a given search term the higher will be that suggestion's // ranking for future uses of that search term. class ShortcutsProvider : public AutocompleteProvider, public ShortcutsBackend::ShortcutsBackendObserver {...}
つまり、SearchProviderでキーワードと一致するサジェスチョンを除いても、ShortcutProviderが最近の検索履歴からキーワードと一致しているサジェスチョンをAutocompleteProviderに流していたということです……
え、RecentlySearchedHistoryProviderとかにすればよくね????ShortcutProviderて命名から想像できなくね????ネーミングおかしくね?????
ということで、原因はこのShortcutProviderから、別のDBから最近の検索履歴データが供給されていたというものでした。 チームでの話し合いの結果、SearchProvider、ShortcutProviderに対して個々にキーワードフィルターをかけるより、全てのProviderの結果をまとめ、重複を除いてソートした後にフィルターをかけた方比較回数を減らせるのではないかと考え、 SortAndDedupMatches()というAutocompleteResultクラスの関数でフィルターをかけることにしました。 変更後のコードは以下のようになります。
void AutocompleteResult::SortAndDedupMatches( metrics::OmniboxEventProto::PageClassification page_classification, ACMatches* matches) { // Sort matches such that duplicate matches are consecutive. matches->sort(DestinationSort<AutocompleteMatch>(page_classification)); // Set duplicate_matches for the first match before erasing duplicate // matches. for (ACMatches::iterator i(matches->begin()); i != matches->end(); ++i) { for (auto j = std::next(i); j != matches->end() && AutocompleteMatch::DestinationsEqual(*i, *j); ++j) { AutocompleteMatch& dup_match(*j); i->duplicate_matches.insert(i->duplicate_matches.end(), dup_match.duplicate_matches.begin(), dup_match.duplicate_matches.end()); dup_match.duplicate_matches.clear(); i->duplicate_matches.push_back(dup_match); } } // Erase duplicate matches. matches->unique(AutocompleteMatch::DestinationsEqual); // 変更ここから for (auto itr=matches->begin(); itr!=matches->end();){ bool be_removed = itr->type == AutocompleteMatchType::HISTORY_TITLE || itr->type == AutocompleteMatchType::HISTORY_BODY || itr->type == AutocompleteMatchType::SEARCH_HISTORY || itr->type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL; if (be_removed && base::EqualsASCII(itr->contents, "PokemonGo") == true){ itr = matches->erase(itr); } else { itr++; } } // 変更ここまで }
単に非表示キーワードを含む全てのサジェスチョンをeraceしてしまうと、普段使いのときに特定のキーワードに関連した検索をしようとしても補完されないので、不便になってしまいますよね。
そこで今回はbe_removed
というbool変数を導入して、補完のタイプがSearchHistory及びそれを自動補完したもの(過去のユーザーの検索ワードに基づいたサジェスチョン)、もしくはHistoryBody(過去に閲覧したURL履歴の本文)に
非表示キーワードが含まれる時のみ、
自動補完の候補から除くことにしました。
これでOmniboxのキーワードフィルタリング機能が完成しました!!
ただこれだとC++のソースコードでキーワードをハードコーディングしているだけなので、次回以降、User Profileを利用してユーザーの登録した非表示キーワードを読み込み、
フィルターをかける部分の変更や複数キーワードの登録などについてまとめます。
大規模ソフトウェアを手探る Chromeのソースコードとドキュメントをひたすら漁る
Chromeの膨大なソースコードから変更するクラスを見つける
Chromeをビルドできたところでいよいよ実際にコードの変更を行なっていきます。 今回私たちが追加しようとした機能はChromeの上部にある検索ボックスに表示される検索履歴に単語フィルターをかける(つまり特定の単語が含まれる検索履歴は表示しない)というものでした。 そこで私たちの考えたアイディアとしては「入力に従って検索ボックス情報を更新しているモジュール上で、特定の単語が含まれる検索履歴は更新するデータに追加しないようにする」もしくは「特定の単語が含まれる検索ワードは検索履歴のデータに保存されないように」しようと言うものでした。 最初は検索ボックス内でヒストリー(過去の検索履歴)を読み出してautocompleteの候補を作成しているクラスでちょっとif文追加するだけの簡単なお仕事かな〜と楽観的に考えていたのですが、現実はそこまで甘くはありませんでした…
いきなりChrome code searchの限界に直面する
まず最初は前回紹介したChrome Code Searchで検索をかけます。とりあえず「history search」で検索をかけてみる… 4400件wwwwwww
これを全部見ていくのはしんどそう…
とりあえずいきなり適当な単語でcode searchはあんまりよくないと言うことに気がつき(それはそう)、まずどういったモジュールがあの検索ボックスの自動補完に対応しているのか、Chromeのドキュメントから調べていくことにしました。
Chromium (Chrome Browser, Chrome OS)の開発者向けドキュメント (design doc) は公式ホームペーシ、The Chromium ProjectのFor Developers>Design Documentsから検索できます。
Google検索していたらChromeの上部にある検索ボックスはomniboxと呼ばれることがわかったので、このomnibox関連のdesign docを調べていくことにしました。
omnibox関連のドキュメントをとりあえずぐぐりまくる
公式のDesign Docsのうち、まず注目したのはOmnibox: History Providerについてのドキュメントです。
One of the autocomplete providers for the omnibox (the HistoryQuickProvider, HQP for short) serves up autocomplete candidates from the profile's history database. ふむふむ…autocomplete providersなるものが自動補完候補をユーザープロファイルのDBから取得していることがわかります。 またこのautocomplete providersにはいくつか種類があることも読み取れます。
次はDesign Docではないのですが、一般向けのOmniboxの説明をしたドキュメントを見つけました。 これによると、Omniboxの自動補完は以下のタイプに分類できるようです。
Search for... This searches for the typed query - this will be the topmost and default item if the input looks like a search string.
URL If the input (after auto-complete, if applicable) looks like a URL, this will repeat the user's input and be the default, top-most item.
Previous URLs If the input matches a previous URL, those URLs will be shown here.
Nav Suggest Uses the user's default search provider to suggest URLs based on the user's input.
Search Suggest Uses the user's default search provider to suggest search terms based on the user's input.
History Results Shows the number of entries in the user's history that match the given input - selecting this item will take the user to the history results page for their given search term.
これを見た感じ検索ワードの履歴を返しているのはHistory Resultsみたいですね…History Resultに関連してそうなSearchProviderクラスが見つかりました。
とりあえずSearchProviderクラスを変更してみる
SearchHistoryクラスを見ていくと、ScoreHistoryResults
という関数が、過去の検索履歴のうち入力文字列とマッチするHistoryResults
ついて、関連度スコアrelevance
をつけてそれに基づいてソートしていることわかりました。ScoreHistoryResults
自体はSearchProvider::SearchHistoryResultHelperという別のヘルパー関数を呼んで、実際に自動補完候補のスコア付などを行なっています。
とりあえず、この関数の中でHistoryResults
から、検索文字列があるキーワードと一致する時にscored_resutsに追加しないようにするように書き換えて見ました。
SearchProvider::ScoreHistoryResultsHelper(const HistoryResults& results, bool base_prevent_inline_autocomplete, bool input_multiple_words, const base::string16& input_text, bool is_keyword) { ... // resultsは過去の検索履歴のうち入力文字列とマッチするものを全て取り出している。 for (HistoryResults::const_iterator i(results.begin()); i != results.end(); ++i) { ... SearchSuggestionParser::SuggestResult history_suggestion( trimmed_suggestion, AutocompleteMatchType::SEARCH_HISTORY, 0, trimmed_suggestion, base::string16(), base::string16(), base::string16(), base::string16(), nullptr, std::string(), std::string(), is_keyword, relevance, false, false, trimmed_input); history_suggestion.set_received_after_last_keystroke(false); // scored_resultsにキーワードとマッチしていない時のみ追加する。 // 変更ここから if (base::EqualsASCII(history_suggestions.suggestion(), “harry”) == false){ scored_results.insert(insertion_position, history_suggestion); } // 変更ここまで } ... }
よしこれでひとまずフィルター機能は実装できたはず…...と思うも、search_providerの返す結果では"harry"が消えているのに表示される結果には"harry"が残っていると言う謎の事態に......
次回のブログではSearchProviderから供給されるデータに対してフィルターをかけるだけではなぜダメだったのか、OmniboxのAutocompleteのデータフローを追いながら原因を解明していきます。
大規模ソフトウェア(Chromium)を手探る 導入・ビルド編
このブログは電気系3年生後期実験の大規模ソフトウェアを手探るの最終報告ブログです。
「大規模ソフトウェアを手探る」とは?
東大工学部電気系3年生は3~6個のテーマの実験を行うことが必修となっており、「大規模ソフトウェアを手探る」とは田浦健二郎先生による「演習レベルの小さなプログラムが作れること」と,「実用規模のプログラムが作れること」のギャップを埋める (ための知識と経験を得ることを目的に、1〜数百万行のオープンソースソフトウェアをソースからビルドし新しい機能を加えたりします。
今回私たちのチームでは、みんなが使う大規模なものをやりたいという気持ちと、個人的にChromeのBlink周りの開発をしたことがあったのでChromiumをテーマに選びました。
そもそもChromiumとは
GoogleのブラウザであるChromeを使っている方も多いのではないでしょうか。ChromiumとはオープンソースソフトウェアとしてChromeを見たときの名称です。
Chromium(クロミウム)はオープンソースのウェブブラウザのプロジェクト名で、Google Chromeはこのソースコードを引き抜いて開発されたものである。 -- Chromium - Wikipedia
ちなみにChromiumがどのくらい大規模かというと、 英語圏の質問投稿サイトQuoraの質問How many lines of code is Google Chrome?によると、2012年の時点で400~500万行のコードが存在しているみたいです。
4,490,488 lines of code, 5,448,668 lines with comments included, spread over 21,367 unique files.
現在の正確なChromiumのソースコード行数については見つけられていないのですが、この数字だけでも私たちが普段使っているブラウザがどれほど大きく複雑なものかわかります。
このブログ記事について
この「大規模ソフトウェアを手探る」実験では、一般的な実験レポートの代わりにこういったブログ形式でのレポートの提出が推奨さえています。
私たちもChromiumという(超)大規模ソフトウェアをて探るにあたって、かなり昨年の方の丁寧なブログレポートが参考になったため、できるだけ詳細に
どういった作業を行っていったのか、何に途中つまづいたのかをまとめています。
Chromiumのソースをいじってみたいという方以外だともしかしたら少し冗長かもしれませんが、今後「大規模ソフトウェアを手探る」でChromiumをやりたい方、
もしくは趣味でChromiumをゴニョゴニョしたい方の参考になれば嬉しいです。
今回の実験で実装しようと思った機能
今回の実装しようと思った機能はChromeの上部にでる検索ボックス上に入力に応じて現れる過去の履歴やサーチエンジンの検索サジェスチョンから、指定したキーワードを含む過去の検索履歴を表示しないようにする機能です。
この検索ボックスのことはOmniboxというChromium BrowserのComponentです。
スライドを投影している時やペアプロの時にChrome検索を開いた時に表示したくないような検索履歴がサジェストされることがあるという話を聞いた(見た)のと、 なんとなく面白そうだからというこれまた雑な理由で決めたのですが、結果的にBrowserのコアの部分からフロントエンドまで、またC++からJavaScriptの世界でどうやりとりがされているか等を知ることができ、 とても勉強になりました。
大まかな作業スケジュールとブログ記事リスト
以下が大まかな作業スケジュールになります。できるだけ来年以降大規模ソフトウェアを手探る人に参考になるように細かく書いたら長くなったのでブログ記事を分割しました。
- 1 ~ 2日目 : gnuplotに機能を追加する全体での実習、チーム決め等、ソースコードのフェッチ、ビルド等。
3 ~ 4日目 : 検索履歴からの自動補完を行なっているクラス(SearchProvider)を特定。
大規模ソフトウェアを手探る Chromeのソースコードとドキュメントをひたすら漁る - あさりさんの作業日記5日目 : キーワードフィルター機能実装
大規模ソフトウェアを手探る 検索ボックスにおける自動補完・サジェスチョンのデータの流れを追う - あさりさんの作業日記6 ~ 7 日目 : User Profileに新たに非表示キーワードを保持する項目を追加。複数キーワードをフィルタリングするための変更追加。またブラウザの設定画面で非表示キーワードを入力できるようにWebUIを変更
大規模ソフトウェア(Chromium)を手探る user profileに設定を追加する - あさりさんの作業日記
大規模ソフトウェア(Chromium)を手探る - 設定画面(settings)を手探る1 - - elechoのぶろぐ
大規模ソフトウェア(Chromium)を手探る - 設定画面(settings)を手探る2 - - elechoのぶろぐ8日目 : 新たに追加された設定画面用のCallbackHandlerクラスを追加。
大規模ソフトウェア(Chromium)を手探る callbackハンドラを追加する・全体の感想 - あさりさんの作業日記9日目 : バグの修正と余分なコメント等の削除
- 10日目 : 発表 docs.google.com
まずはビルドから
とはいうもののまずビルドができないと話になりません。 この記事では開発を始める前のChromiumのソースコードを引っ張ってきて手元のPCでビルドするまでをまとめたいと思います。 今回のチームメンバーはみんなLinux(Ubuntu)を使用していたので、ChromiumのLinux向けコンパイル、ビルドのイントラクションを参考にしました。 基本的には順に従って行けばいいのですが、いくつかハマりポイントがあったのでそれも含めて追っていきます。
depot_toolsをインストールする
depot_toolsはソースコードのチェックアウトや依存パッケージのダウンロードなどの、Chromiumのビルドやコントリビュートに必要な機能を提供してくれます。
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git $ export PATH="$PATH:/path/to/depot_tools"
Chromiumのソースコードを取ってくる
Chromiumのソースコードを取ってきます。まず適当な場所にChromium作業用のディレクトリを作りましょう。 今回は~/crというディレクトリ以下ににChromiumソースを落としてくることを想定しています。
$ mkdir ~/cr && cd cr
次に、先ほどインストールしてきたdepot_toolsのfetchというツールを使ってコードと依存ライブラリをチェックアウトします(ローカルに持ってきます)。
$ fetch --nohooks--no-history chromium
基本的には--no-history
オプションをつけることをオススメします。--no-history
をつけないと、レポジトリの全ての履歴を取ってくるので時間もストレージも倍くらいかかります…
--no-history
をつけた場合はだいたい30分程度でコードがチェックアウトできます。
hookを走らせる
フェッチが完了したらChromium特有のhooksを走らせて必要な追加バイナリをインストールします。
ただUbuntuの場合、この前にフェッチしてきたsrc/以下でbuild/install-build-deps.sh
を走らせる必要があります。これ公式だとあまり大きく書いてないので見落としがちですが、先にこちらを走らせないと「dependenciesがないよ!」って怒られます。
$ cd src
$ build/install-build-deps.sh
$ gclient runhooks
ビルドのためのセットアップをする
必要なダウンロードが終わりました…!いよいよビルドしていきましょう。まず、セットアップをします。
$ gn gen out/Default
次にいよいよninjaでコンパイルし、実行ファイルをRunします。
$ ninja -C out/Default chrome
$ out/Default/chrome
学科の8コアのマシンでだいたい5時間程度で完了しました…さすが大規模ソフトウェア...
電気系3年生は実験の前にビルドまで終らせてから来ような、初回にやることがなくなるからな
ここまででChromeのビルドが完了しました。ここまでは割とトラブルなくできると思います。
次回からは実際に検索フィルター機能を追加するための手順を順にまとめていきます。
Chrome開発に当たって便利なもの
- Chrome code search
Chromiumのコードの検索ができる。個人的にはhistory, xsearch(referenceやcall hierarchy)がすごい便利だと思いました。 - The Chromium Project For Developers Design Documents
Chromeの様々な機能や全体のアーキテクチャのdesign documents(設計書、機能書)が集まっています。500万行以上あるコードから自分の変更したい機能に関わる部分をcode searchのみで発掘するのはなかなか厳しいので、こういったdesign documentsを見て関連するモジュールの実装等を参考にするといいかもしれません。