Changeset 234873 in webkit
- Timestamp:
- Aug 14, 2018 5:30:50 PM (6 years ago)
- Location:
- trunk
- Files:
-
- 15 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/Source/WTF/ChangeLog
r234859 r234873 1 2018-08-14 Alex Christensen <achristensen@webkit.org> 2 3 isValidCSSSelector is unsafe to be called from a non-main thread 4 https://bugs.webkit.org/show_bug.cgi?id=188581 5 <rdar://problem/40517358> 6 7 Reviewed by Sam Weinig. 8 9 * wtf/Vector.h: 10 (WTF::minCapacity>::isolatedCopy): 11 1 12 2018-08-14 Alex Christensen <achristensen@webkit.org> 2 13 -
trunk/Source/WTF/wtf/Vector.h
r233157 r234873 767 767 void clear() { shrinkCapacity(0); } 768 768 769 template<typename U = T> Vector<U> isolatedCopy() const; 770 769 771 ALWAYS_INLINE void append(ValueType&& value) { append<ValueType>(std::forward<ValueType>(value)); } 770 772 template<typename U> void append(U&&); … … 1605 1607 #endif 1606 1608 1609 template<typename T, size_t inlineCapacity, typename OverflowHandler, size_t minCapacity> 1610 template<typename U> 1611 inline Vector<U> Vector<T, inlineCapacity, OverflowHandler, minCapacity>::isolatedCopy() const 1612 { 1613 Vector<U> copy; 1614 copy.reserveInitialCapacity(size()); 1615 for (const auto& element : *this) 1616 copy.uncheckedAppend(element.isolatedCopy()); 1617 return copy; 1618 } 1619 1607 1620 template<typename VectorType, typename Func> 1608 1621 size_t removeRepeatedElements(VectorType& vector, const Func& func) -
trunk/Source/WebCore/ChangeLog
r234870 r234873 1 2018-08-14 Alex Christensen <achristensen@webkit.org> 2 3 isValidCSSSelector is unsafe to be called from a non-main thread 4 https://bugs.webkit.org/show_bug.cgi?id=188581 5 <rdar://problem/40517358> 6 7 Reviewed by Sam Weinig. 8 9 Parsing and determining whether the css selectors are valid is fast enough to do before 10 hopping to the background thread for the slow NFA/DFA operations and writing to disk. 11 Doing it on the main thread avoids the thread safety issues in the CSSParser's use of strings. 12 13 * contentextensions/ContentExtensionCompiler.cpp: 14 (WebCore::ContentExtensions::compileRuleList): 15 * contentextensions/ContentExtensionCompiler.h: 16 * contentextensions/ContentExtensionParser.cpp: 17 (WebCore::ContentExtensions::isValidCSSSelector): 18 (WebCore::ContentExtensions::loadEncodedRules): 19 (WebCore::ContentExtensions::parseRuleList): 20 * contentextensions/ContentExtensionParser.h: 21 * contentextensions/ContentExtensionRule.cpp: 22 (WebCore::ContentExtensions::Trigger::isolatedCopy const): 23 (WebCore::ContentExtensions::Action::isolatedCopy const): 24 * contentextensions/ContentExtensionRule.h: 25 (WebCore::ContentExtensions::Trigger::isEmpty const): 26 (WebCore::ContentExtensions::Trigger::operator== const): 27 (WebCore::ContentExtensions::Action::Action): 28 (WebCore::ContentExtensions::ContentExtensionRule::isolatedCopy const): 29 (WebCore::ContentExtensions::ContentExtensionRule::operator== const): 30 (WebCore::ContentExtensions::vectorIsolatedCopy): 31 1 32 2018-08-14 Ansh Shukla <ansh_shukla@apple.com> 2 33 -
trunk/Source/WebCore/contentextensions/ContentExtensionCompiler.cpp
r229209 r234873 284 284 } 285 285 286 std::error_code compileRuleList(ContentExtensionCompilationClient& client, String&& ruleJSON) 287 { 288 auto ruleList = parseRuleList(WTFMove(ruleJSON)); 289 if (!ruleList.has_value()) 290 return ruleList.error(); 291 Vector<ContentExtensionRule> parsedRuleList = WTFMove(ruleList.value()); 286 std::error_code compileRuleList(ContentExtensionCompilationClient& client, String&& ruleJSON, Vector<ContentExtensionRule>&& parsedRuleList) 287 { 288 #if !ASSERT_DISABLED 289 callOnMainThread([ruleJSON = ruleJSON.isolatedCopy(), parsedRuleList = parsedRuleList.isolatedCopy()] { 290 ASSERT(parseRuleList(ruleJSON) == parsedRuleList); 291 }); 292 #endif 292 293 293 294 bool domainConditionSeen = false; … … 314 315 #endif 315 316 316 client.writeSource( ruleJSON);317 client.writeSource(std::exchange(ruleJSON, String())); 317 318 318 319 Vector<SerializedActionByte> actions; -
trunk/Source/WebCore/contentextensions/ContentExtensionCompiler.h
r223728 r234873 41 41 42 42 // Functions should be called in this order. All except writeActions and finalize can be called multiple times, though. 43 virtual void writeSource( const String&) = 0;43 virtual void writeSource(String&&) = 0; 44 44 virtual void writeActions(Vector<SerializedActionByte>&&, bool conditionsApplyOnlyToDomain) = 0; 45 45 virtual void writeFiltersWithoutConditionsBytecode(Vector<DFABytecode>&&) = 0; … … 49 49 }; 50 50 51 WEBCORE_EXPORT std::error_code compileRuleList(ContentExtensionCompilationClient&, String&& );51 WEBCORE_EXPORT std::error_code compileRuleList(ContentExtensionCompilationClient&, String&& ruleJSON, Vector<ContentExtensionRule>&&); 52 52 53 53 } // namespace ContentExtensions -
trunk/Source/WebCore/contentextensions/ContentExtensionParser.cpp
r233520 r234873 231 231 bool isValidCSSSelector(const String& selector) 232 232 { 233 ASSERT(isMainThread()); 233 234 AtomicString::init(); 234 235 QualifiedName::init(); … … 300 301 } 301 302 302 static Expected<Vector<ContentExtensionRule>, std::error_code> loadEncodedRules(ExecState& exec, String&& ruleJSON)303 static Expected<Vector<ContentExtensionRule>, std::error_code> loadEncodedRules(ExecState& exec, const String& ruleJSON) 303 304 { 304 305 VM& vm = exec.vm(); … … 348 349 } 349 350 350 Expected<Vector<ContentExtensionRule>, std::error_code> parseRuleList( String&& ruleJSON)351 Expected<Vector<ContentExtensionRule>, std::error_code> parseRuleList(const String& ruleJSON) 351 352 { 352 353 #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING … … 359 360 360 361 ExecState* exec = globalObject->globalExec(); 361 auto ruleList = loadEncodedRules(*exec, WTFMove(ruleJSON));362 auto ruleList = loadEncodedRules(*exec, ruleJSON); 362 363 363 364 vm = nullptr; -
trunk/Source/WebCore/contentextensions/ContentExtensionParser.h
r214358 r234873 39 39 class ContentExtensionRule; 40 40 41 Expected<Vector<ContentExtensionRule>, std::error_code> parseRuleList(String&&);41 WEBCORE_EXPORT Expected<Vector<ContentExtensionRule>, std::error_code> parseRuleList(const String&); 42 42 WEBCORE_EXPORT bool isValidCSSSelector(const String&); 43 43 -
trunk/Source/WebCore/contentextensions/ContentExtensionRule.cpp
r222602 r234873 114 114 } 115 115 116 Trigger Trigger::isolatedCopy() const 117 { 118 return { 119 urlFilter.isolatedCopy(), 120 urlFilterIsCaseSensitive, 121 topURLConditionIsCaseSensitive, 122 flags, 123 conditions.isolatedCopy(), 124 conditionType 125 }; 126 } 127 128 Action Action::isolatedCopy() const 129 { 130 return { 131 m_extensionIdentifier.isolatedCopy(), 132 m_type, 133 m_actionID, 134 m_stringArgument.isolatedCopy() 135 }; 136 } 137 116 138 } // namespace ContentExtensions 117 139 -
trunk/Source/WebCore/contentextensions/ContentExtensionRule.h
r222602 r234873 56 56 ConditionType conditionType { ConditionType::None }; 57 57 58 WEBCORE_EXPORT Trigger isolatedCopy() const; 59 58 60 ~Trigger() 59 61 { … … 67 69 return urlFilter.isEmpty() 68 70 && !urlFilterIsCaseSensitive 71 && !topURLConditionIsCaseSensitive 69 72 && !flags 70 73 && conditions.isEmpty() … … 76 79 return urlFilter == other.urlFilter 77 80 && urlFilterIsCaseSensitive == other.urlFilterIsCaseSensitive 81 && topURLConditionIsCaseSensitive == other.topURLConditionIsCaseSensitive 78 82 && flags == other.flags 79 83 && conditions == other.conditions … … 164 168 const String& stringArgument() const { return m_stringArgument; } 165 169 170 WEBCORE_EXPORT Action isolatedCopy() const; 171 166 172 private: 173 Action(String&& extensionIdentifier, ActionType type, uint32_t actionID, String&& stringArgument) 174 : m_extensionIdentifier(WTFMove(extensionIdentifier)) 175 , m_type(type) 176 , m_actionID(actionID) 177 , m_stringArgument(WTFMove(stringArgument)) 178 { } 179 167 180 String m_extensionIdentifier; 168 181 ActionType m_type; … … 173 186 class ContentExtensionRule { 174 187 public: 175 ContentExtensionRule(Trigger&&, Action&&);188 WEBCORE_EXPORT ContentExtensionRule(Trigger&&, Action&&); 176 189 177 190 const Trigger& trigger() const { return m_trigger; } 178 191 const Action& action() const { return m_action; } 192 193 ContentExtensionRule isolatedCopy() const 194 { 195 return { m_trigger.isolatedCopy(), m_action.isolatedCopy() }; 196 } 197 bool operator==(const ContentExtensionRule& other) const 198 { 199 return m_trigger == other.m_trigger && m_action == other.m_action; 200 } 179 201 180 202 private: -
trunk/Source/WebKit/ChangeLog
r234870 r234873 1 2018-08-14 Alex Christensen <achristensen@webkit.org> 2 3 isValidCSSSelector is unsafe to be called from a non-main thread 4 https://bugs.webkit.org/show_bug.cgi?id=188581 5 <rdar://problem/40517358> 6 7 Reviewed by Sam Weinig. 8 9 * UIProcess/API/APIContentRuleListStore.cpp: 10 (API::compiledToFile): 11 (API::ContentRuleListStore::lookupContentRuleList): 12 (API::ContentRuleListStore::getAvailableContentRuleListIdentifiers): 13 (API::ContentRuleListStore::compileContentRuleList): 14 (API::ContentRuleListStore::removeContentRuleList): 15 (API::ContentRuleListStore::getContentRuleListSource): 16 * UIProcess/API/APIContentRuleListStore.h: 17 * UIProcess/API/Cocoa/WKContentRuleListStore.mm: 18 1 19 2018-08-14 Ansh Shukla <ansh_shukla@apple.com> 2 20 -
trunk/Source/WebKit/UIProcess/API/APIContentRuleListStore.cpp
r224900 r234873 36 36 #include <WebCore/ContentExtensionCompiler.h> 37 37 #include <WebCore/ContentExtensionError.h> 38 #include <WebCore/ContentExtensionParser.h> 38 39 #include <WebCore/QualifiedName.h> 39 40 #include <string> 41 #include <wtf/CompletionHandler.h> 40 42 #include <wtf/NeverDestroyed.h> 41 43 #include <wtf/RunLoop.h> … … 210 212 } 211 213 212 static std::error_code compiledToFile(String&& json, const String& finalFilePath, ContentRuleListMetaData& metaData, Data& mappedData)214 static std::error_code compiledToFile(String&& json, Vector<WebCore::ContentExtensions::ContentExtensionRule>&& parsedRules, const String& finalFilePath, ContentRuleListMetaData& metaData, Data& mappedData) 213 215 { 214 216 using namespace WebCore::ContentExtensions; … … 228 230 } 229 231 230 void writeSource(const String& sourceJSON) final { 232 void writeSource(String&& sourceJSON) final 233 { 231 234 ASSERT(!m_filtersWithoutConditionsBytecodeWritten); 232 235 ASSERT(!m_filtersWithConditionBytecodeWritten); … … 340 343 CompilationClient compilationClient(temporaryFileHandle, metaData); 341 344 342 if (auto compilerError = compileRuleList(compilationClient, WTFMove(json) )) {345 if (auto compilerError = compileRuleList(compilationClient, WTFMove(json), WTFMove(parsedRules))) { 343 346 WTFLogAlways("Content Rule List compiling failed: Compiling failed."); 344 347 closeFile(temporaryFileHandle); … … 392 395 } 393 396 394 void ContentRuleListStore::lookupContentRuleList(const WTF::String& identifier, Function<void(RefPtr<API::ContentRuleList>, std::error_code)> completionHandler)397 void ContentRuleListStore::lookupContentRuleList(const WTF::String& identifier, CompletionHandler<void(RefPtr<API::ContentRuleList>, std::error_code)> completionHandler) 395 398 { 396 399 m_readQueue->dispatch([protectedThis = makeRef(*this), identifier = identifier.isolatedCopy(), storePath = m_storePath.isolatedCopy(), legacyFilename = m_legacyFilename, completionHandler = WTFMove(completionHandler)]() mutable { … … 400 403 Data fileData; 401 404 if (!openAndMapContentRuleList(path, metaData, fileData)) { 402 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler)] {405 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler)] () mutable { 403 406 completionHandler(nullptr, Error::LookupFailed); 404 407 }); … … 407 410 408 411 if (metaData.version != ContentRuleListStore::CurrentContentRuleListFileVersion) { 409 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler)] {412 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler)] () mutable { 410 413 completionHandler(nullptr, Error::VersionMismatch); 411 414 }); … … 413 416 } 414 417 415 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), identifier = identifier.isolatedCopy(), fileData = WTFMove(fileData), metaData = WTFMove(metaData), completionHandler = WTFMove(completionHandler)] {418 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), identifier = identifier.isolatedCopy(), fileData = WTFMove(fileData), metaData = WTFMove(metaData), completionHandler = WTFMove(completionHandler)] () mutable { 416 419 completionHandler(createExtension(identifier, metaData, fileData), { }); 417 420 }); … … 419 422 } 420 423 421 void ContentRuleListStore::getAvailableContentRuleListIdentifiers( Function<void(WTF::Vector<WTF::String>)> completionHandler)424 void ContentRuleListStore::getAvailableContentRuleListIdentifiers(CompletionHandler<void(WTF::Vector<WTF::String>)> completionHandler) 422 425 { 423 426 m_readQueue->dispatch([protectedThis = makeRef(*this), storePath = m_storePath.isolatedCopy(), legacyFilename = m_legacyFilename, completionHandler = WTFMove(completionHandler)]() mutable { … … 436 439 } 437 440 438 void ContentRuleListStore::compileContentRuleList(const WTF::String& identifier, WTF::String&& json, Function<void(RefPtr<API::ContentRuleList>, std::error_code)> completionHandler)441 void ContentRuleListStore::compileContentRuleList(const WTF::String& identifier, WTF::String&& json, CompletionHandler<void(RefPtr<API::ContentRuleList>, std::error_code)> completionHandler) 439 442 { 440 443 AtomicString::init(); 441 444 WebCore::QualifiedName::init(); 442 m_compileQueue->dispatch([protectedThis = makeRef(*this), identifier = identifier.isolatedCopy(), legacyFilename = m_legacyFilename, json = json.isolatedCopy(), storePath = m_storePath.isolatedCopy(), completionHandler = WTFMove(completionHandler)] () mutable { 445 446 auto parsedRules = WebCore::ContentExtensions::parseRuleList(json); 447 if (!parsedRules.has_value()) 448 return completionHandler(nullptr, parsedRules.error()); 449 450 m_compileQueue->dispatch([protectedThis = makeRef(*this), identifier = identifier.isolatedCopy(), legacyFilename = m_legacyFilename, json = json.isolatedCopy(), parsedRules = parsedRules.value().isolatedCopy(), storePath = m_storePath.isolatedCopy(), completionHandler = WTFMove(completionHandler)] () mutable { 443 451 auto path = constructedPath(storePath, identifier, legacyFilename); 444 452 445 453 ContentRuleListMetaData metaData; 446 454 Data fileData; 447 auto error = compiledToFile(WTFMove(json), path, metaData, fileData);455 auto error = compiledToFile(WTFMove(json), WTFMove(parsedRules), path, metaData, fileData); 448 456 if (error) { 449 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), error = WTFMove(error), completionHandler = WTFMove(completionHandler)] {457 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), error = WTFMove(error), completionHandler = WTFMove(completionHandler)] () mutable { 450 458 completionHandler(nullptr, error); 451 459 }); … … 453 461 } 454 462 455 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), identifier = WTFMove(identifier), fileData = WTFMove(fileData), metaData = WTFMove(metaData), completionHandler = WTFMove(completionHandler)] {463 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), identifier = WTFMove(identifier), fileData = WTFMove(fileData), metaData = WTFMove(metaData), completionHandler = WTFMove(completionHandler)] () mutable { 456 464 RefPtr<API::ContentRuleList> contentRuleList = createExtension(identifier, metaData, fileData); 457 465 completionHandler(contentRuleList, { }); … … 460 468 } 461 469 462 void ContentRuleListStore::removeContentRuleList(const WTF::String& identifier, Function<void(std::error_code)> completionHandler)470 void ContentRuleListStore::removeContentRuleList(const WTF::String& identifier, CompletionHandler<void(std::error_code)> completionHandler) 463 471 { 464 472 m_removeQueue->dispatch([protectedThis = makeRef(*this), identifier = identifier.isolatedCopy(), storePath = m_storePath.isolatedCopy(), legacyFilename = m_legacyFilename, completionHandler = WTFMove(completionHandler)]() mutable { … … 466 474 467 475 if (!deleteFile(path)) { 468 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler)] {476 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler)] () mutable { 469 477 completionHandler(Error::RemoveFailed); 470 478 }); … … 472 480 } 473 481 474 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler)] {482 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler)] () mutable { 475 483 completionHandler({ }); 476 484 }); … … 495 503 } 496 504 497 void ContentRuleListStore::getContentRuleListSource(const WTF::String& identifier, Function<void(WTF::String)> completionHandler)505 void ContentRuleListStore::getContentRuleListSource(const WTF::String& identifier, CompletionHandler<void(WTF::String)> completionHandler) 498 506 { 499 507 m_readQueue->dispatch([protectedThis = makeRef(*this), identifier = identifier.isolatedCopy(), storePath = m_storePath.isolatedCopy(), legacyFilename = m_legacyFilename, completionHandler = WTFMove(completionHandler)]() mutable { … … 501 509 502 510 auto complete = [protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler)](String source) mutable { 503 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler), source = source.isolatedCopy()] {511 RunLoop::main().dispatch([protectedThis = WTFMove(protectedThis), completionHandler = WTFMove(completionHandler), source = source.isolatedCopy()] () mutable { 504 512 completionHandler(source); 505 513 }); -
trunk/Source/WebKit/UIProcess/API/APIContentRuleListStore.h
r223286 r234873 63 63 virtual ~ContentRuleListStore(); 64 64 65 void compileContentRuleList(const WTF::String& identifier, WTF::String&& json, Function<void(RefPtr<API::ContentRuleList>, std::error_code)>);66 void lookupContentRuleList(const WTF::String& identifier, Function<void(RefPtr<API::ContentRuleList>, std::error_code)>);67 void removeContentRuleList(const WTF::String& identifier, Function<void(std::error_code)>);68 void getAvailableContentRuleListIdentifiers( Function<void(WTF::Vector<WTF::String>)>);65 void compileContentRuleList(const WTF::String& identifier, WTF::String&& json, CompletionHandler<void(RefPtr<API::ContentRuleList>, std::error_code)>); 66 void lookupContentRuleList(const WTF::String& identifier, CompletionHandler<void(RefPtr<API::ContentRuleList>, std::error_code)>); 67 void removeContentRuleList(const WTF::String& identifier, CompletionHandler<void(std::error_code)>); 68 void getAvailableContentRuleListIdentifiers(CompletionHandler<void(WTF::Vector<WTF::String>)>); 69 69 70 70 // For testing only. 71 71 void synchronousRemoveAllContentRuleLists(); 72 72 void invalidateContentRuleListVersion(const WTF::String& identifier); 73 void getContentRuleListSource(const WTF::String& identifier, Function<void(WTF::String)>);73 void getContentRuleListSource(const WTF::String& identifier, CompletionHandler<void(WTF::String)>); 74 74 75 75 private: -
trunk/Source/WebKit/UIProcess/API/Cocoa/WKContentRuleListStore.mm
r216809 r234873 33 33 #import "WKErrorInternal.h" 34 34 #import <wtf/BlockPtr.h> 35 #import <wtf/CompletionHandler.h> 35 36 36 37 static WKErrorCode toWKErrorCode(const std::error_code& error) -
trunk/Tools/ChangeLog
r234870 r234873 1 2018-08-14 Alex Christensen <achristensen@webkit.org> 2 3 isValidCSSSelector is unsafe to be called from a non-main thread 4 https://bugs.webkit.org/show_bug.cgi?id=188581 5 <rdar://problem/40517358> 6 7 Reviewed by Sam Weinig. 8 9 * TestWebKitAPI/Tests/WebCore/ContentExtensions.cpp: 10 (TestWebKitAPI::InMemoryCompiledContentExtension::create): 11 (TestWebKitAPI::checkCompilerError): 12 1 13 2018-08-14 Ansh Shukla <ansh_shukla@apple.com> 2 14 -
trunk/Tools/TestWebKitAPI/Tests/WebCore/ContentExtensions.cpp
r233122 r234873 101 101 102 102 private: 103 void writeSource( const String&) final { }103 void writeSource(String&&) final { } 104 104 105 105 void writeActions(Vector<ContentExtensions::SerializedActionByte>&& actions, bool conditionsApplyOnlyToDomain) final … … 151 151 CompiledContentExtensionData extensionData; 152 152 InMemoryContentExtensionCompilationClient client(extensionData); 153 auto compilerError = ContentExtensions::compileRuleList(client, WTFMove(filter)); 153 auto parsedRules = ContentExtensions::parseRuleList(filter); 154 auto compilerError = ContentExtensions::compileRuleList(client, WTFMove(filter), WTFMove(parsedRules.value())); 154 155 155 156 // Compiling should always succeed here. We have other tests for compile failures. … … 1372 1373 CompiledContentExtensionData extensionData; 1373 1374 InMemoryContentExtensionCompilationClient client(extensionData); 1374 std::error_code compilerError = ContentExtensions::compileRuleList(client, json); 1375 auto parsedRules = ContentExtensions::parseRuleList(json); 1376 std::error_code compilerError; 1377 if (parsedRules.has_value()) 1378 compilerError = ContentExtensions::compileRuleList(client, json, WTFMove(parsedRules.value())); 1379 else 1380 compilerError = parsedRules.error(); 1375 1381 EXPECT_EQ(compilerError.value(), expectedError.value()); 1376 1382 if (compilerError.value())
Note: See TracChangeset
for help on using the changeset viewer.