Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(763)

Side by Side Diff: components/history/core/browser/thumbnail_database.cc

Issue 2727553006: [sql] Convert thumbnails and top-sites databases to auto-recovery. (Closed)
Patch Set: git-cl-format Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "components/history/core/browser/thumbnail_database.h" 5 #include "components/history/core/browser/thumbnail_database.h"
6 6
7 #include <stddef.h> 7 #include <stddef.h>
8 #include <stdint.h> 8 #include <stdint.h>
9 #include <algorithm> 9 #include <algorithm>
10 #include <string> 10 #include <string>
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after
75 namespace { 75 namespace {
76 76
77 // For this database, schema migrations are deprecated after two 77 // For this database, schema migrations are deprecated after two
78 // years. This means that the oldest non-deprecated version should be 78 // years. This means that the oldest non-deprecated version should be
79 // two years old or greater (thus the migrations to get there are 79 // two years old or greater (thus the migrations to get there are
80 // older). Databases containing deprecated versions will be cleared 80 // older). Databases containing deprecated versions will be cleared
81 // at startup. Since this database is a cache, losing old data is not 81 // at startup. Since this database is a cache, losing old data is not
82 // fatal (in fact, very old data may be expired immediately at startup 82 // fatal (in fact, very old data may be expired immediately at startup
83 // anyhow). 83 // anyhow).
84 84
85 // Version 8: ???????? by rogerm@chromium.org on 2015-??-?? 85 // Version 8: 982ef2c1/r323176 by rogerm@chromium.org on 2015-03-31
86 // Version 7: 911a634d/r209424 by qsr@chromium.org on 2013-07-01 86 // Version 7: 911a634d/r209424 by qsr@chromium.org on 2013-07-01
87 // Version 6: 610f923b/r152367 by pkotwicz@chromium.org on 2012-08-20 87 // Version 6: 610f923b/r152367 by pkotwicz@chromium.org on 2012-08-20 (depr.)
pwnall 2017/03/04 01:35:32 I think this change broke a test?
Scott Hess - ex-Googler 2017/03/07 01:34:31 Yeah, brain fart, I tested the recovery case but n
pwnall 2017/03/16 18:55:44 Sorry for being unclear. I didn't mean to say you
88 // Version 5: e2ee8ae9/r105004 by groby@chromium.org on 2011-10-12 (deprecated) 88 // Version 5: e2ee8ae9/r105004 by groby@chromium.org on 2011-10-12 (deprecated)
89 // Version 4: 5f104d76/r77288 by sky@chromium.org on 2011-03-08 (deprecated) 89 // Version 4: 5f104d76/r77288 by sky@chromium.org on 2011-03-08 (deprecated)
90 // Version 3: 09911bf3/r15 by initial.commit on 2008-07-26 (deprecated) 90 // Version 3: 09911bf3/r15 by initial.commit on 2008-07-26 (deprecated)
91 91
92 // Version number of the database. 92 // Version number of the database.
93 // NOTE(shess): When changing the version, add a new golden file for 93 // NOTE(shess): When changing the version, add a new golden file for
94 // the new version and a test to verify that Init() works with it. 94 // the new version and a test to verify that Init() works with it.
95 const int kCurrentVersionNumber = 8; 95 const int kCurrentVersionNumber = 8;
96 const int kCompatibleVersionNumber = 8; 96 const int kCompatibleVersionNumber = 8;
97 const int kDeprecatedVersionNumber = 5; // and earlier. 97 const int kDeprecatedVersionNumber = 6; // and earlier.
pwnall 2017/03/04 01:35:32 I think this change broke a test. https://build.ch
98 98
99 void FillIconMapping(const sql::Statement& statement, 99 void FillIconMapping(const sql::Statement& statement,
100 const GURL& page_url, 100 const GURL& page_url,
101 IconMapping* icon_mapping) { 101 IconMapping* icon_mapping) {
102 icon_mapping->mapping_id = statement.ColumnInt64(0); 102 icon_mapping->mapping_id = statement.ColumnInt64(0);
103 icon_mapping->icon_id = statement.ColumnInt64(1); 103 icon_mapping->icon_id = statement.ColumnInt64(1);
104 icon_mapping->icon_type = 104 icon_mapping->icon_type =
105 static_cast<favicon_base::IconType>(statement.ColumnInt(2)); 105 static_cast<favicon_base::IconType>(statement.ColumnInt(2));
106 icon_mapping->icon_url = GURL(statement.ColumnString(3)); 106 icon_mapping->icon_url = GURL(statement.ColumnString(3));
107 icon_mapping->page_url = page_url; 107 icon_mapping->page_url = page_url;
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
139 // TODO(shess): If this could be related to the time in the channel, then the 139 // TODO(shess): If this could be related to the time in the channel, then the
140 // rate could ramp up over time. Perhaps could remember the timestamp the 140 // rate could ramp up over time. Perhaps could remember the timestamp the
141 // first time upload is considered, and ramp up 1% per day? 141 // first time upload is considered, and ramp up 1% per day?
142 static const uint64_t kReportPercent = 5; 142 static const uint64_t kReportPercent = 5;
143 uint64_t rand = base::RandGenerator(100); 143 uint64_t rand = base::RandGenerator(100);
144 if (rand <= kReportPercent) 144 if (rand <= kReportPercent)
145 db->ReportDiagnosticInfo(extended_error, stmt); 145 db->ReportDiagnosticInfo(extended_error, stmt);
146 } 146 }
147 147
148 // NOTE(shess): Schema modifications must consider initial creation in 148 // NOTE(shess): Schema modifications must consider initial creation in
149 // |InitImpl()|, recovery in |RecoverDatabaseOrRaze()|, and history pruning in 149 // |InitImpl()| and history pruning in |RetainDataForPageUrls()|.
150 // |RetainDataForPageUrls()|.
151 bool InitTables(sql::Connection* db) { 150 bool InitTables(sql::Connection* db) {
152 const char kIconMappingSql[] = 151 const char kIconMappingSql[] =
153 "CREATE TABLE IF NOT EXISTS icon_mapping" 152 "CREATE TABLE IF NOT EXISTS icon_mapping"
154 "(" 153 "("
155 "id INTEGER PRIMARY KEY," 154 "id INTEGER PRIMARY KEY,"
156 "page_url LONGVARCHAR NOT NULL," 155 "page_url LONGVARCHAR NOT NULL,"
157 "icon_id INTEGER" 156 "icon_id INTEGER"
158 ")"; 157 ")";
159 if (!db->Execute(kIconMappingSql)) 158 if (!db->Execute(kIconMappingSql))
160 return false; 159 return false;
(...skipping 22 matching lines...) Expand all
183 // the same layout. 182 // the same layout.
184 "last_requested INTEGER DEFAULT 0" 183 "last_requested INTEGER DEFAULT 0"
185 ")"; 184 ")";
186 if (!db->Execute(kFaviconBitmapsSql)) 185 if (!db->Execute(kFaviconBitmapsSql))
187 return false; 186 return false;
188 187
189 return true; 188 return true;
190 } 189 }
191 190
192 // NOTE(shess): Schema modifications must consider initial creation in 191 // NOTE(shess): Schema modifications must consider initial creation in
193 // |InitImpl()|, recovery in |RecoverDatabaseOrRaze()|, and history pruning in 192 // |InitImpl()| and history pruning in |RetainDataForPageUrls()|.
194 // |RetainDataForPageUrls()|.
195 bool InitIndices(sql::Connection* db) { 193 bool InitIndices(sql::Connection* db) {
196 const char kIconMappingUrlIndexSql[] = 194 const char kIconMappingUrlIndexSql[] =
197 "CREATE INDEX IF NOT EXISTS icon_mapping_page_url_idx" 195 "CREATE INDEX IF NOT EXISTS icon_mapping_page_url_idx"
198 " ON icon_mapping(page_url)"; 196 " ON icon_mapping(page_url)";
199 const char kIconMappingIdIndexSql[] = 197 const char kIconMappingIdIndexSql[] =
200 "CREATE INDEX IF NOT EXISTS icon_mapping_icon_id_idx" 198 "CREATE INDEX IF NOT EXISTS icon_mapping_icon_id_idx"
201 " ON icon_mapping(icon_id)"; 199 " ON icon_mapping(icon_id)";
202 if (!db->Execute(kIconMappingUrlIndexSql) || 200 if (!db->Execute(kIconMappingUrlIndexSql) ||
203 !db->Execute(kIconMappingIdIndexSql)) { 201 !db->Execute(kIconMappingIdIndexSql)) {
204 return false; 202 return false;
205 } 203 }
206 204
207 const char kFaviconsIndexSql[] = 205 const char kFaviconsIndexSql[] =
208 "CREATE INDEX IF NOT EXISTS favicons_url ON favicons(url)"; 206 "CREATE INDEX IF NOT EXISTS favicons_url ON favicons(url)";
209 if (!db->Execute(kFaviconsIndexSql)) 207 if (!db->Execute(kFaviconsIndexSql))
210 return false; 208 return false;
211 209
212 const char kFaviconBitmapsIndexSql[] = 210 const char kFaviconBitmapsIndexSql[] =
213 "CREATE INDEX IF NOT EXISTS favicon_bitmaps_icon_id ON " 211 "CREATE INDEX IF NOT EXISTS favicon_bitmaps_icon_id ON "
214 "favicon_bitmaps(icon_id)"; 212 "favicon_bitmaps(icon_id)";
215 if (!db->Execute(kFaviconBitmapsIndexSql)) 213 if (!db->Execute(kFaviconBitmapsIndexSql))
216 return false; 214 return false;
217 215
218 return true; 216 return true;
219 } 217 }
220 218
221 enum RecoveryEventType {
222 RECOVERY_EVENT_RECOVERED = 0,
223 RECOVERY_EVENT_FAILED_SCOPER,
224 RECOVERY_EVENT_FAILED_META_VERSION_ERROR, // obsolete
225 RECOVERY_EVENT_FAILED_META_VERSION_NONE, // obsolete
226 RECOVERY_EVENT_FAILED_META_WRONG_VERSION6, // obsolete
227 RECOVERY_EVENT_FAILED_META_WRONG_VERSION5, // obsolete
228 RECOVERY_EVENT_FAILED_META_WRONG_VERSION,
229 RECOVERY_EVENT_FAILED_RECOVER_META, // obsolete
230 RECOVERY_EVENT_FAILED_META_INSERT, // obsolete
231 RECOVERY_EVENT_FAILED_INIT,
232 RECOVERY_EVENT_FAILED_RECOVER_FAVICONS, // obsolete
233 RECOVERY_EVENT_FAILED_FAVICONS_INSERT, // obsolete
234 RECOVERY_EVENT_FAILED_RECOVER_FAVICON_BITMAPS, // obsolete
235 RECOVERY_EVENT_FAILED_FAVICON_BITMAPS_INSERT, // obsolete
236 RECOVERY_EVENT_FAILED_RECOVER_ICON_MAPPING, // obsolete
237 RECOVERY_EVENT_FAILED_ICON_MAPPING_INSERT, // obsolete
238 RECOVERY_EVENT_RECOVERED_VERSION6, // obsolete
239 RECOVERY_EVENT_FAILED_META_INIT,
240 RECOVERY_EVENT_FAILED_META_VERSION,
241 RECOVERY_EVENT_DEPRECATED,
242 RECOVERY_EVENT_FAILED_V5_INITSCHEMA, // obsolete
243 RECOVERY_EVENT_FAILED_V5_AUTORECOVER_FAVICONS, // obsolete
244 RECOVERY_EVENT_FAILED_V5_AUTORECOVER_ICON_MAPPING, // obsolete
245 RECOVERY_EVENT_RECOVERED_VERSION5, // obsolete
246 RECOVERY_EVENT_FAILED_AUTORECOVER_FAVICONS,
247 RECOVERY_EVENT_FAILED_AUTORECOVER_FAVICON_BITMAPS,
248 RECOVERY_EVENT_FAILED_AUTORECOVER_ICON_MAPPING,
249 RECOVERY_EVENT_FAILED_COMMIT,
250
251 // Always keep this at the end.
252 RECOVERY_EVENT_MAX,
253 };
254
255 void RecordRecoveryEvent(RecoveryEventType recovery_event) {
256 UMA_HISTOGRAM_ENUMERATION("History.FaviconsRecovery",
257 recovery_event, RECOVERY_EVENT_MAX);
258 }
259
260 // Recover the database to the extent possible, razing it if recovery
261 // is not possible.
262 // TODO(shess): This is mostly just a safe proof of concept. In the
263 // real world, this database is probably not worthwhile recovering, as
264 // opposed to just razing it and starting over whenever corruption is
265 // detected. So this database is a good test subject.
266 void RecoverDatabaseOrRaze(sql::Connection* db, const base::FilePath& db_path) {
267 // NOTE(shess): This code is currently specific to the version
268 // number. I am working on simplifying things to loosen the
269 // dependency, meanwhile contact me if you need to bump the version.
270 DCHECK_EQ(8, kCurrentVersionNumber);
271
272 // TODO(shess): Reset back after?
273 db->reset_error_callback();
274
275 // For histogram purposes.
276 size_t favicons_rows_recovered = 0;
277 size_t favicon_bitmaps_rows_recovered = 0;
278 size_t icon_mapping_rows_recovered = 0;
279 int64_t original_size = 0;
280 base::GetFileSize(db_path, &original_size);
281
282 std::unique_ptr<sql::Recovery> recovery = sql::Recovery::Begin(db, db_path);
283 if (!recovery) {
284 // TODO(shess): Unable to create recovery connection. This
285 // implies something substantial is wrong. At this point |db| has
286 // been poisoned so there is nothing really to do.
287 //
288 // Possible responses are unclear. If the failure relates to a
289 // problem somehow specific to the temporary file used to back the
290 // database, then an in-memory database could possibly be used.
291 // This could potentially allow recovering the main database, and
292 // might be simple to implement w/in Begin().
293 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_SCOPER);
294 return;
295 }
296
297 // Setup the meta recovery table and fetch the version number from
298 // the corrupt database.
299 int version = 0;
300 if (!recovery->SetupMeta() || !recovery->GetMetaVersionNumber(&version)) {
301 // TODO(shess): Prior histograms indicate all failures are in
302 // creating the recover virtual table for corrupt.meta. The table
303 // may not exist, or the database may be too far gone. Either
304 // way, unclear how to resolve.
305 sql::Recovery::Rollback(std::move(recovery));
306 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_META_VERSION);
307 return;
308 }
309
310 // This code may be able to fetch version information that the regular
311 // deprecation path cannot.
312 // NOTE(shess,rogerm): v6 is not currently deprecated in the normal Init()
313 // path, but is deprecated in the recovery path in the interest of keeping
314 // the code simple. http://crbug.com/327485 for numbers.
315 DCHECK_LE(kDeprecatedVersionNumber, 6);
316 if (version <= 6) {
317 sql::Recovery::Unrecoverable(std::move(recovery));
318 RecordRecoveryEvent(RECOVERY_EVENT_DEPRECATED);
319 return;
320 }
321
322 // Earlier versions have been handled or deprecated.
323 if (version < 7) {
324 sql::Recovery::Unrecoverable(std::move(recovery));
325 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_META_WRONG_VERSION);
326 return;
327 }
328
329 // Recover to current schema version.
330 sql::MetaTable recover_meta_table;
331 if (!recover_meta_table.Init(recovery->db(), kCurrentVersionNumber,
332 kCompatibleVersionNumber)) {
333 sql::Recovery::Rollback(std::move(recovery));
334 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_META_INIT);
335 return;
336 }
337
338 // Create a fresh version of the database. The recovery code uses
339 // conflict-resolution to handle duplicates, so the indices are
340 // necessary.
341 if (!InitTables(recovery->db()) || !InitIndices(recovery->db())) {
342 // TODO(shess): Unable to create the new schema in the new
343 // database. The new database should be a temporary file, so
344 // being unable to work with it is pretty unclear.
345 //
346 // What are the potential responses, even? The recovery database
347 // could be opened as in-memory. If the temp database had a
348 // filesystem problem and the temp filesystem differs from the
349 // main database, then that could fix it.
350 sql::Recovery::Rollback(std::move(recovery));
351 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_INIT);
352 return;
353 }
354
355 if (!recovery->AutoRecoverTable("favicons", &favicons_rows_recovered)) {
356 sql::Recovery::Rollback(std::move(recovery));
357 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_AUTORECOVER_FAVICONS);
358 return;
359 }
360 if (!recovery->AutoRecoverTable("favicon_bitmaps",
361 &favicon_bitmaps_rows_recovered)) {
362 sql::Recovery::Rollback(std::move(recovery));
363 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_AUTORECOVER_FAVICON_BITMAPS);
364 return;
365 }
366 if (!recovery->AutoRecoverTable("icon_mapping",
367 &icon_mapping_rows_recovered)) {
368 sql::Recovery::Rollback(std::move(recovery));
369 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_AUTORECOVER_ICON_MAPPING);
370 return;
371 }
372
373 // TODO(shess): Is it possible/likely to have broken foreign-key
374 // issues with the tables?
375 // - icon_mapping.icon_id maps to no favicons.id
376 // - favicon_bitmaps.icon_id maps to no favicons.id
377 // - favicons.id is referenced by no icon_mapping.icon_id
378 // - favicons.id is referenced by no favicon_bitmaps.icon_id
379 // This step is possibly not worth the effort necessary to develop
380 // and sequence the statements, as it is basically a form of garbage
381 // collection.
382
383 if (!sql::Recovery::Recovered(std::move(recovery))) {
384 RecordRecoveryEvent(RECOVERY_EVENT_FAILED_COMMIT);
385 return;
386 }
387
388 // Track the size of the recovered database relative to the size of
389 // the input database. The size should almost always be smaller,
390 // unless the input database was empty to start with. If the
391 // percentage results are very low, something is awry.
392 int64_t final_size = 0;
393 if (original_size > 0 &&
394 base::GetFileSize(db_path, &final_size) &&
395 final_size > 0) {
396 int percentage = static_cast<int>(original_size * 100 / final_size);
397 UMA_HISTOGRAM_PERCENTAGE("History.FaviconsRecoveredPercentage",
398 std::max(100, percentage));
399 }
400
401 // Using 10,000 because these cases mostly care about "none
402 // recovered" and "lots recovered". More than 10,000 rows recovered
403 // probably means there's something wrong with the profile.
404 UMA_HISTOGRAM_COUNTS_10000("History.FaviconsRecoveredRowsFavicons",
405 static_cast<int>(favicons_rows_recovered));
406 UMA_HISTOGRAM_COUNTS_10000("History.FaviconsRecoveredRowsFaviconBitmaps",
407 static_cast<int>(favicon_bitmaps_rows_recovered));
408 UMA_HISTOGRAM_COUNTS_10000("History.FaviconsRecoveredRowsIconMapping",
409 static_cast<int>(icon_mapping_rows_recovered));
410
411 RecordRecoveryEvent(RECOVERY_EVENT_RECOVERED);
412 }
413
414 void DatabaseErrorCallback(sql::Connection* db, 219 void DatabaseErrorCallback(sql::Connection* db,
415 const base::FilePath& db_path, 220 const base::FilePath& db_path,
416 HistoryBackendClient* backend_client, 221 HistoryBackendClient* backend_client,
417 int extended_error, 222 int extended_error,
418 sql::Statement* stmt) { 223 sql::Statement* stmt) {
419 // TODO(shess): Assert that this is running on a safe thread. 224 // TODO(shess): Assert that this is running on a safe thread.
420 // AFAICT, should be the history thread, but at this level I can't 225 // AFAICT, should be the history thread, but at this level I can't
421 // see how to reach that. 226 // see how to reach that.
422 227
423 if (backend_client && backend_client->ShouldReportDatabaseError()) { 228 if (backend_client && backend_client->ShouldReportDatabaseError()) {
424 GenerateDiagnostics(db, extended_error, stmt); 229 GenerateDiagnostics(db, extended_error, stmt);
425 } 230 }
426 231
427 // Attempt to recover corrupt databases. 232 // Attempt to recover corrupt databases.
428 int error = (extended_error & 0xFF); 233 if (sql::Recovery::ShouldRecover(extended_error)) {
429 if (error == SQLITE_CORRUPT || 234 // NOTE(shess): This approach is valid as of version 8. When bumping the
430 error == SQLITE_CANTOPEN || 235 // version, it will PROBABLY remain valid, but consider whether any schema
431 error == SQLITE_NOTADB) { 236 // changes might break automated recovery.
432 RecoverDatabaseOrRaze(db, db_path); 237 DCHECK_EQ(8, kCurrentVersionNumber);
pwnall 2017/03/04 01:35:32 Would it make sense to break this into a DCHECK_GE
Scott Hess - ex-Googler 2017/03/07 01:34:31 I'm trying to craft a revised comment, and running
pwnall 2017/03/16 18:55:44 Thanks for explaining! IIUC, you want to force a d
238
239 // Prevent reentrant calls.
240 db->reset_error_callback();
241
242 // TODO(shess): Is it possible/likely to have broken foreign-key
243 // issues with the tables?
244 // - icon_mapping.icon_id maps to no favicons.id
245 // - favicon_bitmaps.icon_id maps to no favicons.id
246 // - favicons.id is referenced by no icon_mapping.icon_id
247 // - favicons.id is referenced by no favicon_bitmaps.icon_id
248 // This step is possibly not worth the effort necessary to develop
249 // and sequence the statements, as it is basically a form of garbage
250 // collection.
251
252 // After this call, the |db| handle is poisoned so that future calls will
253 // return errors until the handle is re-opened.
254 sql::Recovery::RecoverDatabaseWithMetaVersion(db, db_path);
255
256 // The DLOG(FATAL) below is intended to draw immediate attention to errors
257 // in newly-written code. Database corruption is generally a result of OS
258 // or hardware issues, not coding errors at the client level, so displaying
259 // the error would probably lead to confusion. The ignored call signals the
260 // test-expectation framework that the error was handled.
261 ignore_result(sql::Connection::IsExpectedSqliteError(extended_error));
262 return;
433 } 263 }
434 264
435 // The default handling is to assert on debug and to ignore on release. 265 // The default handling is to assert on debug and to ignore on release.
436 if (!sql::Connection::IsExpectedSqliteError(extended_error)) 266 if (!sql::Connection::IsExpectedSqliteError(extended_error))
437 DLOG(FATAL) << db->GetErrorMessage(); 267 DLOG(FATAL) << db->GetErrorMessage();
438 } 268 }
439 269
440 } // namespace 270 } // namespace
441 271
442 ThumbnailDatabase::IconMappingEnumerator::IconMappingEnumerator() { 272 ThumbnailDatabase::IconMappingEnumerator::IconMappingEnumerator() {
(...skipping 794 matching lines...) Expand 10 before | Expand all | Expand 10 after
1237 meta_table_.SetVersionNumber(8); 1067 meta_table_.SetVersionNumber(8);
1238 meta_table_.SetCompatibleVersionNumber(std::min(8, kCompatibleVersionNumber)); 1068 meta_table_.SetCompatibleVersionNumber(std::min(8, kCompatibleVersionNumber));
1239 return true; 1069 return true;
1240 } 1070 }
1241 1071
1242 bool ThumbnailDatabase::IsFaviconDBStructureIncorrect() { 1072 bool ThumbnailDatabase::IsFaviconDBStructureIncorrect() {
1243 return !db_.IsSQLValid("SELECT id, url, icon_type FROM favicons"); 1073 return !db_.IsSQLValid("SELECT id, url, icon_type FROM favicons");
1244 } 1074 }
1245 1075
1246 } // namespace history 1076 } // namespace history
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698