Changeset 55002 in webkit
- Timestamp:
- Feb 18, 2010 10:23:25 PM (14 years ago)
- Location:
- trunk/JavaScriptCore
- Files:
-
- 9 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/JavaScriptCore/ChangeLog
r54988 r55002 1 2010-02-18 Oliver Hunt <oliver@apple.com> 2 3 Reviewed by Gavin Barraclough. 4 5 Improve interpreter getter performance 6 https://bugs.webkit.org/show_bug.cgi?id=35138 7 8 Improve the performance of getter dispatch by making it possible 9 for the interpreter to cache the GetterSetter object lookup. 10 11 To do this we simply need to make PropertySlot aware of getters 12 as a potentially cacheable property, and record the base and this 13 objects for a getter access. This allows us to use more-or-less 14 identical code to that used by the normal get_by_id caching, with 15 the dispatch being the only actual difference. 16 17 I'm holding off of implementing this in the JIT until I do some 18 cleanup to try and making coding in the JIT not be as horrible 19 as it is currently. 20 21 * bytecode/CodeBlock.cpp: 22 (JSC::CodeBlock::dump): 23 (JSC::CodeBlock::derefStructures): 24 (JSC::CodeBlock::refStructures): 25 * bytecode/Opcode.h: 26 * interpreter/Interpreter.cpp: 27 (JSC::Interpreter::resolveGlobal): 28 (JSC::Interpreter::tryCacheGetByID): 29 (JSC::Interpreter::privateExecute): 30 * jit/JIT.cpp: 31 (JSC::JIT::privateCompileMainPass): 32 * jit/JITStubs.cpp: 33 (JSC::JITThunks::tryCacheGetByID): 34 (JSC::DEFINE_STUB_FUNCTION): 35 * runtime/JSObject.cpp: 36 (JSC::JSObject::fillGetterPropertySlot): 37 * runtime/PropertySlot.cpp: 38 (JSC::PropertySlot::functionGetter): 39 * runtime/PropertySlot.h: 40 (JSC::PropertySlot::isGetter): 41 (JSC::PropertySlot::isCacheable): 42 (JSC::PropertySlot::isCacheableValue): 43 (JSC::PropertySlot::setValueSlot): 44 (JSC::PropertySlot::setGetterSlot): 45 (JSC::PropertySlot::setCacheableGetterSlot): 46 (JSC::PropertySlot::clearOffset): 47 (JSC::PropertySlot::thisValue): 48 1 49 2010-02-17 Geoffrey Garen <ggaren@apple.com> 2 50 -
trunk/JavaScriptCore/bytecode/CodeBlock.cpp
r54789 r55002 766 766 break; 767 767 } 768 case op_get_by_id_getter_self: { 769 printGetByIdOp(exec, location, it, "get_by_id_getter_self"); 770 break; 771 } 772 case op_get_by_id_getter_self_list: { 773 printGetByIdOp(exec, location, it, "get_by_id_getter_self_list"); 774 break; 775 } 776 case op_get_by_id_getter_proto: { 777 printGetByIdOp(exec, location, it, "get_by_id_getter_proto"); 778 break; 779 } 780 case op_get_by_id_getter_proto_list: { 781 printGetByIdOp(exec, location, it, "get_by_id_getter_proto_list"); 782 break; 783 } 784 case op_get_by_id_getter_chain: { 785 printGetByIdOp(exec, location, it, "get_by_id_getter_chain"); 786 break; 787 } 768 788 case op_get_by_id_generic: { 769 789 printGetByIdOp(exec, location, it, "get_by_id_generic"); … … 1356 1376 Interpreter* interpreter = m_globalData->interpreter; 1357 1377 1358 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_self) ) {1378 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_self) || vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_getter_self)) { 1359 1379 vPC[4].u.structure->deref(); 1360 1380 return; 1361 1381 } 1362 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_proto) ) {1382 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_proto) || vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_getter_proto)) { 1363 1383 vPC[4].u.structure->deref(); 1364 1384 vPC[5].u.structure->deref(); 1365 1385 return; 1366 1386 } 1367 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_chain) ) {1387 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_chain) || vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_getter_chain)) { 1368 1388 vPC[4].u.structure->deref(); 1369 1389 vPC[5].u.structureChain->deref(); … … 1386 1406 } 1387 1407 if ((vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_proto_list)) 1388 || (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_self_list))) { 1408 || (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_self_list)) 1409 || (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_getter_proto_list)) 1410 || (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_getter_self_list))) { 1389 1411 PolymorphicAccessStructureList* polymorphicStructures = vPC[4].u.polymorphicStructures; 1390 1412 polymorphicStructures->derefStructures(vPC[5].u.operand); … … 1401 1423 Interpreter* interpreter = m_globalData->interpreter; 1402 1424 1403 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_self) ) {1425 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_self) || vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_getter_self)) { 1404 1426 vPC[4].u.structure->ref(); 1405 1427 return; 1406 1428 } 1407 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_proto) ) {1429 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_proto) || vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_getter_proto)) { 1408 1430 vPC[4].u.structure->ref(); 1409 1431 vPC[5].u.structure->ref(); 1410 1432 return; 1411 1433 } 1412 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_chain) ) {1434 if (vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_chain) || vPC[0].u.opcode == interpreter->getOpcode(op_get_by_id_getter_chain)) { 1413 1435 vPC[4].u.structure->ref(); 1414 1436 vPC[5].u.structureChain->ref(); -
trunk/JavaScriptCore/bytecode/Opcode.h
r53891 r55002 105 105 macro(op_get_by_id_proto_list, 8) \ 106 106 macro(op_get_by_id_chain, 8) \ 107 macro(op_get_by_id_getter_self, 8) \ 108 macro(op_get_by_id_getter_self_list, 8) \ 109 macro(op_get_by_id_getter_proto, 8) \ 110 macro(op_get_by_id_getter_proto_list, 8) \ 111 macro(op_get_by_id_getter_chain, 8) \ 107 112 macro(op_get_by_id_generic, 8) \ 108 113 macro(op_get_array_length, 8) \ -
trunk/JavaScriptCore/interpreter/Interpreter.cpp
r54843 r55002 41 41 #include "EvalCodeCache.h" 42 42 #include "ExceptionHelpers.h" 43 #include "GetterSetter.h" 43 44 #include "GlobalEvalFunction.h" 44 45 #include "JSActivation.h" … … 170 171 if (globalObject->getPropertySlot(callFrame, ident, slot)) { 171 172 JSValue result = slot.getValue(callFrame, ident); 172 if (slot.isCacheable () && !globalObject->structure()->isUncacheableDictionary() && slot.slotBase() == globalObject) {173 if (slot.isCacheableValue() && !globalObject->structure()->isUncacheableDictionary() && slot.slotBase() == globalObject) { 173 174 if (vPC[4].u.structure) 174 175 vPC[4].u.structure->deref(); … … 1030 1031 1031 1032 if (slot.slotBase() == baseValue) { 1032 vPC[0] = getOpcode(op_get_by_id_self);1033 vPC[0] = slot.isGetter() ? getOpcode(op_get_by_id_getter_self) : getOpcode(op_get_by_id_self); 1033 1034 vPC[5] = slot.cachedOffset(); 1034 1035 … … 1057 1058 ASSERT(!baseObject->structure()->isUncacheableDictionary()); 1058 1059 1059 vPC[0] = getOpcode(op_get_by_id_proto);1060 vPC[0] = slot.isGetter() ? getOpcode(op_get_by_id_getter_proto) : getOpcode(op_get_by_id_proto); 1060 1061 vPC[5] = baseObject->structure(); 1061 1062 vPC[6] = offset; … … 1072 1073 } 1073 1074 1074 vPC[0] = getOpcode(op_get_by_id_chain);1075 vPC[0] = slot.isGetter() ? getOpcode(op_get_by_id_getter_chain) : getOpcode(op_get_by_id_chain); 1075 1076 vPC[4] = structure; 1076 1077 vPC[5] = structure->prototypeChain(callFrame); … … 2162 2163 NEXT_INSTRUCTION(); 2163 2164 } 2165 #if HAVE(COMPUTED_GOTO) 2166 goto *(&&skip_id_getter_proto); 2167 #endif 2168 DEFINE_OPCODE(op_get_by_id_getter_proto) { 2169 /* op_get_by_id_getter_proto dst(r) base(r) property(id) structure(sID) prototypeStructure(sID) offset(n) nop(n) 2170 2171 Cached property access: Attempts to get a cached getter property from the 2172 value base's prototype. If the cache misses, op_get_by_id_getter_proto 2173 reverts to op_get_by_id. 2174 */ 2175 int base = vPC[2].u.operand; 2176 JSValue baseValue = callFrame->r(base).jsValue(); 2177 2178 if (LIKELY(baseValue.isCell())) { 2179 JSCell* baseCell = asCell(baseValue); 2180 Structure* structure = vPC[4].u.structure; 2181 2182 if (LIKELY(baseCell->structure() == structure)) { 2183 ASSERT(structure->prototypeForLookup(callFrame).isObject()); 2184 JSObject* protoObject = asObject(structure->prototypeForLookup(callFrame)); 2185 Structure* prototypeStructure = vPC[5].u.structure; 2186 2187 if (LIKELY(protoObject->structure() == prototypeStructure)) { 2188 int dst = vPC[1].u.operand; 2189 int offset = vPC[6].u.operand; 2190 if (GetterSetter* getterSetter = asGetterSetter(protoObject->getDirectOffset(offset).asCell())) { 2191 JSObject* getter = getterSetter->getter(); 2192 CallData callData; 2193 CallType callType = getter->getCallData(callData); 2194 JSValue result = call(callFrame, getter, callType, callData, asObject(baseCell), ArgList()); 2195 CHECK_FOR_EXCEPTION(); 2196 callFrame->r(dst) = result; 2197 } else 2198 callFrame->r(dst) = jsUndefined(); 2199 vPC += OPCODE_LENGTH(op_get_by_id_getter_proto); 2200 NEXT_INSTRUCTION(); 2201 } 2202 } 2203 } 2204 uncacheGetByID(callFrame->codeBlock(), vPC); 2205 NEXT_INSTRUCTION(); 2206 } 2207 #if HAVE(COMPUTED_GOTO) 2208 skip_id_getter_proto: 2209 #endif 2164 2210 DEFINE_OPCODE(op_get_by_id_self_list) { 2165 2211 // Polymorphic self access caching currently only supported when JITting. … … 2176 2222 NEXT_INSTRUCTION(); 2177 2223 } 2224 DEFINE_OPCODE(op_get_by_id_getter_self_list) { 2225 // Polymorphic self access caching currently only supported when JITting. 2226 ASSERT_NOT_REACHED(); 2227 // This case of the switch must not be empty, else (op_get_by_id_self_list == op_get_by_id_chain)! 2228 vPC += OPCODE_LENGTH(op_get_by_id_self_list); 2229 NEXT_INSTRUCTION(); 2230 } 2231 DEFINE_OPCODE(op_get_by_id_getter_proto_list) { 2232 // Polymorphic prototype access caching currently only supported when JITting. 2233 ASSERT_NOT_REACHED(); 2234 // This case of the switch must not be empty, else (op_get_by_id_proto_list == op_get_by_id_chain)! 2235 vPC += OPCODE_LENGTH(op_get_by_id_proto_list); 2236 NEXT_INSTRUCTION(); 2237 } 2178 2238 DEFINE_OPCODE(op_get_by_id_chain) { 2179 2239 /* op_get_by_id_chain dst(r) base(r) property(id) structure(sID) structureChain(chain) count(n) offset(n) … … 2222 2282 NEXT_INSTRUCTION(); 2223 2283 } 2284 #if HAVE(COMPUTED_GOTO) 2285 goto *(&&skip_id_getter_self); 2286 #endif 2287 DEFINE_OPCODE(op_get_by_id_getter_self) { 2288 /* op_get_by_id_self dst(r) base(r) property(id) structure(sID) offset(n) nop(n) nop(n) 2289 2290 Cached property access: Attempts to get a cached property from the 2291 value base. If the cache misses, op_get_by_id_getter_self reverts to 2292 op_get_by_id. 2293 */ 2294 int base = vPC[2].u.operand; 2295 JSValue baseValue = callFrame->r(base).jsValue(); 2296 2297 if (LIKELY(baseValue.isCell())) { 2298 JSCell* baseCell = asCell(baseValue); 2299 Structure* structure = vPC[4].u.structure; 2300 2301 if (LIKELY(baseCell->structure() == structure)) { 2302 ASSERT(baseCell->isObject()); 2303 JSObject* baseObject = asObject(baseCell); 2304 int dst = vPC[1].u.operand; 2305 int offset = vPC[5].u.operand; 2306 2307 if (GetterSetter* getterSetter = asGetterSetter(baseObject->getDirectOffset(offset).asCell())) { 2308 JSObject* getter = getterSetter->getter(); 2309 CallData callData; 2310 CallType callType = getter->getCallData(callData); 2311 JSValue result = call(callFrame, getter, callType, callData, baseObject, ArgList()); 2312 CHECK_FOR_EXCEPTION(); 2313 callFrame->r(dst) = result; 2314 } else 2315 callFrame->r(dst) = jsUndefined(); 2316 2317 vPC += OPCODE_LENGTH(op_get_by_id_getter_self); 2318 NEXT_INSTRUCTION(); 2319 } 2320 } 2321 uncacheGetByID(callFrame->codeBlock(), vPC); 2322 NEXT_INSTRUCTION(); 2323 } 2324 #if HAVE(COMPUTED_GOTO) 2325 skip_id_getter_self: 2326 #endif 2224 2327 DEFINE_OPCODE(op_get_by_id_generic) { 2225 2328 /* op_get_by_id_generic dst(r) base(r) property(id) nop(sID) nop(n) nop(n) nop(n) … … 2242 2345 NEXT_INSTRUCTION(); 2243 2346 } 2347 #if HAVE(COMPUTED_GOTO) 2348 goto *(&&skip_id_getter_chain); 2349 #endif 2350 DEFINE_OPCODE(op_get_by_id_getter_chain) { 2351 /* op_get_by_id_getter_chain dst(r) base(r) property(id) structure(sID) structureChain(chain) count(n) offset(n) 2352 2353 Cached property access: Attempts to get a cached property from the 2354 value base's prototype chain. If the cache misses, op_get_by_id_getter_chain 2355 reverts to op_get_by_id. 2356 */ 2357 int base = vPC[2].u.operand; 2358 JSValue baseValue = callFrame->r(base).jsValue(); 2359 2360 if (LIKELY(baseValue.isCell())) { 2361 JSCell* baseCell = asCell(baseValue); 2362 Structure* structure = vPC[4].u.structure; 2363 2364 if (LIKELY(baseCell->structure() == structure)) { 2365 RefPtr<Structure>* it = vPC[5].u.structureChain->head(); 2366 size_t count = vPC[6].u.operand; 2367 RefPtr<Structure>* end = it + count; 2368 2369 while (true) { 2370 JSObject* baseObject = asObject(baseCell->structure()->prototypeForLookup(callFrame)); 2371 2372 if (UNLIKELY(baseObject->structure() != (*it).get())) 2373 break; 2374 2375 if (++it == end) { 2376 int dst = vPC[1].u.operand; 2377 int offset = vPC[7].u.operand; 2378 if (GetterSetter* getterSetter = asGetterSetter(baseObject->getDirectOffset(offset).asCell())) { 2379 JSObject* getter = getterSetter->getter(); 2380 CallData callData; 2381 CallType callType = getter->getCallData(callData); 2382 JSValue result = call(callFrame, getter, callType, callData, asObject(baseCell), ArgList()); 2383 CHECK_FOR_EXCEPTION(); 2384 callFrame->r(dst) = result; 2385 } else 2386 callFrame->r(dst) = jsUndefined(); 2387 vPC += OPCODE_LENGTH(op_get_by_id_getter_chain); 2388 NEXT_INSTRUCTION(); 2389 } 2390 2391 // Update baseCell, so that next time around the loop we'll pick up the prototype's prototype. 2392 baseCell = baseObject; 2393 } 2394 } 2395 } 2396 uncacheGetByID(callFrame->codeBlock(), vPC); 2397 NEXT_INSTRUCTION(); 2398 } 2399 #if HAVE(COMPUTED_GOTO) 2400 skip_id_getter_chain: 2401 #endif 2244 2402 DEFINE_OPCODE(op_get_array_length) { 2245 2403 /* op_get_array_length dst(r) base(r) property(id) nop(sID) nop(n) nop(n) nop(n) -
trunk/JavaScriptCore/jit/JIT.cpp
r54747 r55002 323 323 case op_get_by_id_self: 324 324 case op_get_by_id_self_list: 325 case op_get_by_id_getter_chain: 326 case op_get_by_id_getter_proto: 327 case op_get_by_id_getter_proto_list: 328 case op_get_by_id_getter_self: 329 case op_get_by_id_getter_self_list: 325 330 case op_get_string_length: 326 331 case op_put_by_id_generic: -
trunk/JavaScriptCore/jit/JITStubs.cpp
r54843 r55002 857 857 858 858 // Uncacheable: give up. 859 if (!slot.isCacheable ()) {859 if (!slot.isCacheableValue()) { 860 860 ctiPatchCallByReturnAddress(codeBlock, returnAddress, FunctionPtr(cti_op_get_by_id_generic)); 861 861 return; 862 862 } 863 ASSERT(!slot.isGetter()); 863 864 864 865 JSCell* baseCell = asCell(baseValue); … … 1290 1291 // be an object. (Assertion to ensure asObject() call below is safe, which comes after 1291 1292 // an isCacheable() chceck. 1292 ASSERT(!slot.isCacheable () || slot.slotBase().isObject());1293 ASSERT(!slot.isCacheableValue() || slot.slotBase().isObject()); 1293 1294 1294 1295 // Check that: … … 1301 1302 JSObject* slotBaseObject; 1302 1303 if (baseValue.isCell() 1303 && slot.isCacheable ()1304 && slot.isCacheableValue() 1304 1305 && !(structure = asCell(baseValue)->structure())->isUncacheableDictionary() 1305 1306 && (slotBaseObject = asObject(slot.slotBase()))->getPropertySpecificValue(callFrame, ident, specific) … … 1375 1376 1376 1377 if (baseValue.isCell() 1377 && slot.isCacheable ()1378 && slot.isCacheableValue() 1378 1379 && !asCell(baseValue)->structure()->isUncacheableDictionary() 1379 1380 && slot.slotBase() == baseValue) { … … 1448 1449 CHECK_FOR_EXCEPTION(); 1449 1450 1450 if (!baseValue.isCell() || !slot.isCacheable () || asCell(baseValue)->structure()->isDictionary()) {1451 if (!baseValue.isCell() || !slot.isCacheableValue() || asCell(baseValue)->structure()->isDictionary()) { 1451 1452 ctiPatchCallByReturnAddress(callFrame->codeBlock(), STUB_RETURN_ADDRESS, FunctionPtr(cti_op_get_by_id_proto_fail)); 1452 1453 return JSValue::encode(result); … … 2304 2305 if (globalObject->getPropertySlot(callFrame, ident, slot)) { 2305 2306 JSValue result = slot.getValue(callFrame, ident); 2306 if (slot.isCacheable () && !globalObject->structure()->isUncacheableDictionary() && slot.slotBase() == globalObject) {2307 if (slot.isCacheableValue() && !globalObject->structure()->isUncacheableDictionary() && slot.slotBase() == globalObject) { 2307 2308 GlobalResolveInfo& globalResolveInfo = callFrame->codeBlock()->globalResolveInfo(globalResolveInfoIndex); 2308 2309 if (globalResolveInfo.structure) -
trunk/JavaScriptCore/runtime/JSObject.cpp
r53170 r55002 517 517 NEVER_INLINE void JSObject::fillGetterPropertySlot(PropertySlot& slot, JSValue* location) 518 518 { 519 if (JSObject* getterFunction = asGetterSetter(*location)->getter()) 520 slot.setGetterSlot(getterFunction); 521 else 519 if (JSObject* getterFunction = asGetterSetter(*location)->getter()) { 520 if (!structure()->isDictionary()) 521 slot.setCacheableGetterSlot(this, getterFunction, offsetForLocation(location)); 522 else 523 slot.setGetterSlot(getterFunction); 524 } else 522 525 slot.setUndefined(); 523 526 } -
trunk/JavaScriptCore/runtime/PropertySlot.cpp
r47236 r55002 36 36 CallType callType = slot.m_data.getterFunc->getCallData(callData); 37 37 if (callType == CallTypeHost) 38 return callData.native.function(exec, slot.m_data.getterFunc, slot. slotBase(), exec->emptyList());38 return callData.native.function(exec, slot.m_data.getterFunc, slot.thisValue(), exec->emptyList()); 39 39 ASSERT(callType == CallTypeJS); 40 40 // FIXME: Can this be done more efficiently using the callData? 41 return asFunction(slot.m_data.getterFunc)->call(exec, slot. slotBase(), exec->emptyList());41 return asFunction(slot.m_data.getterFunc)->call(exec, slot.thisValue(), exec->emptyList()); 42 42 } 43 43 -
trunk/JavaScriptCore/runtime/PropertySlot.h
r46598 r55002 72 72 } 73 73 74 bool isCacheable() const { return m_offset != WTF::notFound; } 74 bool isGetter() const { return m_isGetter; } 75 bool isCacheable() const { return m_isCacheable; } 76 bool isCacheableValue() const { return m_isCacheable && !m_isGetter; } 75 77 size_t cachedOffset() const 76 78 { … … 103 105 m_data.valueSlot = valueSlot; 104 106 m_offset = offset; 107 m_isCacheable = true; 108 m_isGetter = false; 105 109 } 106 110 … … 140 144 m_data.index = index; 141 145 } 142 146 143 147 void setGetterSlot(JSObject* getterFunc) 148 { 149 ASSERT(getterFunc); 150 m_thisValue = m_slotBase; 151 m_getValue = functionGetter; 152 m_data.getterFunc = getterFunc; 153 m_isGetter = true; 154 } 155 156 void setCacheableGetterSlot(JSValue slotBase, JSObject* getterFunc, unsigned offset) 144 157 { 145 158 ASSERT(getterFunc); 146 159 m_getValue = functionGetter; 160 m_thisValue = m_slotBase; 161 m_slotBase = slotBase; 147 162 m_data.getterFunc = getterFunc; 148 } 149 163 m_offset = offset; 164 m_isCacheable = true; 165 m_isGetter = true; 166 } 167 150 168 void setUndefined() 151 169 { … … 183 201 // Clear offset even in release builds, in case this PropertySlot has been used before. 184 202 // (For other data members, we don't need to clear anything because reuse would meaningfully overwrite them.) 185 m_offset = WTF::notFound; 203 m_offset = 0; 204 m_isCacheable = false; 205 m_isGetter = false; 186 206 } 187 207 188 208 unsigned index() const { return m_data.index; } 189 209 210 JSValue thisValue() const { return m_thisValue; } 190 211 private: 191 212 static JSValue functionGetter(ExecState*, const Identifier&, const PropertySlot&); … … 202 223 203 224 JSValue m_value; 225 JSValue m_thisValue; 204 226 205 227 size_t m_offset; 228 bool m_isCacheable : 1; 229 bool m_isGetter : 1; 206 230 }; 207 231
Note: See TracChangeset
for help on using the changeset viewer.