Changeset 224838 in webkit for trunk/Source/JavaScriptCore/tools


Ignore:
Timestamp:
Nov 14, 2017, 1:16:24 PM (8 years ago)
Author:
[email protected]
Message:

Remove JSDollarVMPrototype.
https://bugs.webkit.org/show_bug.cgi?id=179685

Reviewed by Saam Barati.

  1. Move the JSDollarVMPrototype C++ utility functions into VMInspector.cpp.

This allows us to call these functions during lldb debugging sessions using
VMInspector::foo() instead of JSDollarVMPrototype::foo(). It makes sense that
VMInspector provides VM debugging utility methods. It doesn't make sense to
have a JSDollarVMPrototype object provide these methods.

Plus, it's shorter to type VMInspector than JSDollarVMPrototype.

  1. Move the JSDollarVMPrototype JS functions into JSDollarVM.cpp.

JSDollarVM is a special object used only for debugging purposes. There's no
gain in requiring its methods to be stored in a prototype object other than to
conform to typical JS convention. We can remove this complexity.

  • JavaScriptCore.xcodeproj/project.pbxproj:
  • Sources.txt:
  • runtime/JSGlobalObject.cpp:

(JSC::JSGlobalObject::init):

  • tools/JSDollarVM.cpp:

(JSC::JSDollarVM::addFunction):
(JSC::functionCrash):
(JSC::functionDFGTrue):
(JSC::CallerFrameJITTypeFunctor::CallerFrameJITTypeFunctor):
(JSC::CallerFrameJITTypeFunctor::operator() const):
(JSC::CallerFrameJITTypeFunctor::jitType):
(JSC::functionLLintTrue):
(JSC::functionJITTrue):
(JSC::functionGC):
(JSC::functionEdenGC):
(JSC::functionCodeBlockForFrame):
(JSC::codeBlockFromArg):
(JSC::functionCodeBlockFor):
(JSC::functionPrintSourceFor):
(JSC::functionPrintBytecodeFor):
(JSC::functionPrint):
(JSC::functionPrintCallFrame):
(JSC::functionPrintStack):
(JSC::functionValue):
(JSC::functionGetPID):
(JSC::JSDollarVM::finishCreation):

  • tools/JSDollarVM.h:

(JSC::JSDollarVM::create):

  • tools/JSDollarVMPrototype.cpp: Removed.
  • tools/JSDollarVMPrototype.h: Removed.
  • tools/VMInspector.cpp:

(JSC::VMInspector::currentThreadOwnsJSLock):
(JSC::ensureCurrentThreadOwnsJSLock):
(JSC::VMInspector::gc):
(JSC::VMInspector::edenGC):
(JSC::VMInspector::isInHeap):
(JSC::CellAddressCheckFunctor::CellAddressCheckFunctor):
(JSC::CellAddressCheckFunctor::operator() const):
(JSC::VMInspector::isValidCell):
(JSC::VMInspector::isValidCodeBlock):
(JSC::VMInspector::codeBlockForFrame):
(JSC::PrintFrameFunctor::PrintFrameFunctor):
(JSC::PrintFrameFunctor::operator() const):
(JSC::VMInspector::printCallFrame):
(JSC::VMInspector::printStack):
(JSC::VMInspector::printValue):

  • tools/VMInspector.h:
Location:
trunk/Source/JavaScriptCore/tools
Files:
2 deleted
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/JavaScriptCore/tools/JSDollarVM.cpp

    r217108 r224838  
    11/*
    2  * Copyright (C) 2015 Apple Inc. All rights reserved.
     2 * Copyright (C) 2015-2017 Apple Inc. All rights reserved.
    33 *
    44 * Redistribution and use in source and binary forms, with or without
     
    2727#include "JSDollarVM.h"
    2828
     29#include "CodeBlock.h"
     30#include "FunctionCodeBlock.h"
    2931#include "JSCInlines.h"
     32#include "VMInspector.h"
     33#include <wtf/DataLog.h>
     34#include <wtf/ProcessID.h>
     35#include <wtf/StringPrintStream.h>
    3036
    3137namespace JSC {
     
    3339const ClassInfo JSDollarVM::s_info = { "DollarVM", &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDollarVM) };
    3440
     41void JSDollarVM::addFunction(VM& vm, JSGlobalObject* globalObject, const char* name, NativeFunction function, unsigned arguments)
     42{
     43    Identifier identifier = Identifier::fromString(&vm, name);
     44    putDirect(vm, identifier, JSFunction::create(vm, globalObject, arguments, identifier.string(), function));
     45}
     46
     47// Triggers a crash immediately.
     48// Usage: $vm.crash()
     49static EncodedJSValue JSC_HOST_CALL functionCrash(ExecState*)
     50{
     51    CRASH();
     52    return JSValue::encode(jsUndefined());
     53}
     54
     55// Returns true if the current frame is a DFG frame.
     56// Usage: isDFG = $vm.dfgTrue()
     57static EncodedJSValue JSC_HOST_CALL functionDFGTrue(ExecState*)
     58{
     59    return JSValue::encode(jsBoolean(false));
     60}
     61
     62class CallerFrameJITTypeFunctor {
     63public:
     64    CallerFrameJITTypeFunctor()
     65        : m_currentFrame(0)
     66        , m_jitType(JITCode::None)
     67    {
     68    }
     69
     70    StackVisitor::Status operator()(StackVisitor& visitor) const
     71    {
     72        if (m_currentFrame++ > 1) {
     73            m_jitType = visitor->codeBlock()->jitType();
     74            return StackVisitor::Done;
     75        }
     76        return StackVisitor::Continue;
     77    }
     78   
     79    JITCode::JITType jitType() { return m_jitType; }
     80
     81private:
     82    mutable unsigned m_currentFrame;
     83    mutable JITCode::JITType m_jitType;
     84};
     85
     86// Returns true if the current frame is a LLInt frame.
     87// Usage: isLLInt = $vm.llintTrue()
     88static EncodedJSValue JSC_HOST_CALL functionLLintTrue(ExecState* exec)
     89{
     90    if (!exec)
     91        return JSValue::encode(jsUndefined());
     92    CallerFrameJITTypeFunctor functor;
     93    exec->iterate(functor);
     94    return JSValue::encode(jsBoolean(functor.jitType() == JITCode::InterpreterThunk));
     95}
     96
     97// Returns true if the current frame is a baseline JIT frame.
     98// Usage: isBaselineJIT = $vm.jitTrue()
     99static EncodedJSValue JSC_HOST_CALL functionJITTrue(ExecState* exec)
     100{
     101    if (!exec)
     102        return JSValue::encode(jsUndefined());
     103    CallerFrameJITTypeFunctor functor;
     104    exec->iterate(functor);
     105    return JSValue::encode(jsBoolean(functor.jitType() == JITCode::BaselineJIT));
     106}
     107
     108// Runs a full GC synchronously.
     109// Usage: $vm.gc()
     110static EncodedJSValue JSC_HOST_CALL functionGC(ExecState* exec)
     111{
     112    VMInspector::gc(exec);
     113    return JSValue::encode(jsUndefined());
     114}
     115
     116// Runs the edenGC synchronously.
     117// Usage: $vm.edenGC()
     118static EncodedJSValue JSC_HOST_CALL functionEdenGC(ExecState* exec)
     119{
     120    VMInspector::edenGC(exec);
     121    return JSValue::encode(jsUndefined());
     122}
     123
     124// Gets a token for the CodeBlock for a specified frame index.
     125// Usage: codeBlockToken = $vm.codeBlockForFrame(0) // frame 0 is the top frame.
     126static EncodedJSValue JSC_HOST_CALL functionCodeBlockForFrame(ExecState* exec)
     127{
     128    if (exec->argumentCount() < 1)
     129        return JSValue::encode(jsUndefined());
     130
     131    JSValue value = exec->uncheckedArgument(0);
     132    if (!value.isUInt32())
     133        return JSValue::encode(jsUndefined());
     134
     135    // We need to inc the frame number because the caller would consider
     136    // its own frame as frame 0. Hence, we need discount the frame for this
     137    // function.
     138    unsigned frameNumber = value.asUInt32() + 1;
     139    CodeBlock* codeBlock = VMInspector::codeBlockForFrame(exec, frameNumber);
     140    // Though CodeBlock is a JSCell, it is not safe to return it directly back to JS code
     141    // as it is an internal type that the JS code cannot handle. Hence, we first encode the
     142    // CodeBlock* as a double token (which is safe for JS code to handle) before returning it.
     143    return JSValue::encode(JSValue(bitwise_cast<double>(static_cast<uint64_t>(reinterpret_cast<uintptr_t>(codeBlock)))));
     144}
     145
     146static CodeBlock* codeBlockFromArg(ExecState* exec)
     147{
     148    VM& vm = exec->vm();
     149    if (exec->argumentCount() < 1)
     150        return nullptr;
     151
     152    JSValue value = exec->uncheckedArgument(0);
     153    CodeBlock* candidateCodeBlock = nullptr;
     154    if (value.isCell()) {
     155        JSFunction* func = jsDynamicCast<JSFunction*>(vm, value.asCell());
     156        if (func) {
     157            if (func->isHostFunction())
     158                candidateCodeBlock = nullptr;
     159            else
     160                candidateCodeBlock = func->jsExecutable()->eitherCodeBlock();
     161        }
     162    } else if (value.isDouble()) {
     163        // If the value is a double, it may be an encoded CodeBlock* that came from
     164        // $vm.codeBlockForFrame(). We'll treat it as a candidate codeBlock and check if it's
     165        // valid below before using.
     166        candidateCodeBlock = reinterpret_cast<CodeBlock*>(bitwise_cast<uint64_t>(value.asDouble()));
     167    }
     168
     169    if (candidateCodeBlock && VMInspector::isValidCodeBlock(exec, candidateCodeBlock))
     170        return candidateCodeBlock;
     171
     172    if (candidateCodeBlock)
     173        dataLog("Invalid codeBlock: ", RawPointer(candidateCodeBlock), " ", value, "\n");
     174    else
     175        dataLog("Invalid codeBlock: ", value, "\n");
     176    return nullptr;
     177}
     178
     179// Usage: print("codeblock = " + $vm.codeBlockFor(functionObj))
     180// Usage: print("codeblock = " + $vm.codeBlockFor(codeBlockToken))
     181static EncodedJSValue JSC_HOST_CALL functionCodeBlockFor(ExecState* exec)
     182{
     183    CodeBlock* codeBlock = codeBlockFromArg(exec);
     184    WTF::StringPrintStream stream;
     185    if (codeBlock) {
     186        stream.print(*codeBlock);
     187        return JSValue::encode(jsString(exec, stream.toString()));
     188    }
     189    return JSValue::encode(jsUndefined());
     190}
     191
     192// Usage: $vm.printSourceFor(functionObj)
     193// Usage: $vm.printSourceFor(codeBlockToken)
     194static EncodedJSValue JSC_HOST_CALL functionPrintSourceFor(ExecState* exec)
     195{
     196    CodeBlock* codeBlock = codeBlockFromArg(exec);
     197    if (codeBlock)
     198        codeBlock->dumpSource();
     199    return JSValue::encode(jsUndefined());
     200}
     201
     202// Usage: $vm.printBytecodeFor(functionObj)
     203// Usage: $vm.printBytecode(codeBlockToken)
     204static EncodedJSValue JSC_HOST_CALL functionPrintBytecodeFor(ExecState* exec)
     205{
     206    CodeBlock* codeBlock = codeBlockFromArg(exec);
     207    if (codeBlock)
     208        codeBlock->dumpBytecode();
     209    return JSValue::encode(jsUndefined());
     210}
     211
     212// Prints a series of comma separate strings without inserting a newline.
     213// Usage: $vm.print(str1, str2, str3)
     214static EncodedJSValue JSC_HOST_CALL functionPrint(ExecState* exec)
     215{
     216    auto scope = DECLARE_THROW_SCOPE(exec->vm());
     217    for (unsigned i = 0; i < exec->argumentCount(); ++i) {
     218        String argStr = exec->uncheckedArgument(i).toWTFString(exec);
     219        RETURN_IF_EXCEPTION(scope, encodedJSValue());
     220        dataLog(argStr);
     221    }
     222    return JSValue::encode(jsUndefined());
     223}
     224
     225// Prints the current CallFrame.
     226// Usage: $vm.printCallFrame()
     227static EncodedJSValue JSC_HOST_CALL functionPrintCallFrame(ExecState* exec)
     228{
     229    // When the callers call this function, they are expecting to print their
     230    // own frame. So skip 1 for this frame.
     231    VMInspector::printCallFrame(exec, 1);
     232    return JSValue::encode(jsUndefined());
     233}
     234
     235// Prints the JS stack.
     236// Usage: $vm.printStack()
     237static EncodedJSValue JSC_HOST_CALL functionPrintStack(ExecState* exec)
     238{
     239    // When the callers call this function, they are expecting to print the
     240    // stack starting their own frame. So skip 1 for this frame.
     241    VMInspector::printStack(exec, 1);
     242    return JSValue::encode(jsUndefined());
     243}
     244
     245// Gets the dataLog dump of a given JS value as a string.
     246// Usage: print("value = " + $vm.value(jsValue))
     247static EncodedJSValue JSC_HOST_CALL functionValue(ExecState* exec)
     248{
     249    WTF::StringPrintStream stream;
     250    for (unsigned i = 0; i < exec->argumentCount(); ++i) {
     251        if (i)
     252            stream.print(", ");
     253        stream.print(exec->uncheckedArgument(i));
     254    }
     255   
     256    return JSValue::encode(jsString(exec, stream.toString()));
     257}
     258
     259// Gets the pid of the current process.
     260// Usage: print("pid = " + $vm.getpid())
     261static EncodedJSValue JSC_HOST_CALL functionGetPID(ExecState*)
     262{
     263    return JSValue::encode(jsNumber(getCurrentProcessID()));
     264}
     265
     266void JSDollarVM::finishCreation(VM& vm, JSGlobalObject* globalObject)
     267{
     268    Base::finishCreation(vm);
     269   
     270    addFunction(vm, globalObject, "crash", functionCrash, 0);
     271   
     272    putDirectNativeFunction(vm, globalObject, Identifier::fromString(&vm, "dfgTrue"), 0, functionDFGTrue, DFGTrueIntrinsic, static_cast<unsigned>(PropertyAttribute::DontEnum));
     273   
     274    addFunction(vm, globalObject, "llintTrue", functionLLintTrue, 0);
     275    addFunction(vm, globalObject, "jitTrue", functionJITTrue, 0);
     276   
     277    addFunction(vm, globalObject, "gc", functionGC, 0);
     278    addFunction(vm, globalObject, "edenGC", functionEdenGC, 0);
     279   
     280    addFunction(vm, globalObject, "codeBlockFor", functionCodeBlockFor, 1);
     281    addFunction(vm, globalObject, "codeBlockForFrame", functionCodeBlockForFrame, 1);
     282    addFunction(vm, globalObject, "printSourceFor", functionPrintSourceFor, 1);
     283    addFunction(vm, globalObject, "printBytecodeFor", functionPrintBytecodeFor, 1);
     284   
     285    addFunction(vm, globalObject, "print", functionPrint, 1);
     286    addFunction(vm, globalObject, "printCallFrame", functionPrintCallFrame, 0);
     287    addFunction(vm, globalObject, "printStack", functionPrintStack, 0);
     288
     289    addFunction(vm, globalObject, "value", functionValue, 1);
     290    addFunction(vm, globalObject, "getpid", functionGetPID, 0);
     291}
     292
    35293} // namespace JSC
  • trunk/Source/JavaScriptCore/tools/JSDollarVM.h

    r206525 r224838  
    11/*
    2  * Copyright (C) 2015 Apple Inc. All rights reserved.
     2 * Copyright (C) 2015-2017 Apple Inc. All rights reserved.
    33 *
    44 * Redistribution and use in source and binary forms, with or without
     
    4040        return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
    4141    }
    42    
    43     static JSDollarVM* create(VM& vm, Structure* structure)
     42
     43    static JSDollarVM* create(VM& vm, JSGlobalObject* globalObject, Structure* structure)
    4444    {
    4545        JSDollarVM* instance = new (NotNull, allocateCell<JSDollarVM>(vm.heap)) JSDollarVM(vm, structure);
    46         instance->finishCreation(vm);
     46        instance->finishCreation(vm, globalObject);
    4747        return instance;
    4848    }
     
    5353    {
    5454    }
     55
     56    void finishCreation(VM&, JSGlobalObject*);
     57    void addFunction(VM&, JSGlobalObject*, const char* name, NativeFunction, unsigned arguments);
    5558};
    5659
  • trunk/Source/JavaScriptCore/tools/VMInspector.cpp

    r214571 r224838  
    3030#include "CodeBlockSet.h"
    3131#include "HeapInlines.h"
     32#include "HeapIterationScope.h"
    3233#include "MachineContext.h"
     34#include "MarkedSpaceInlines.h"
     35#include "StackVisitor.h"
    3336#include <mutex>
    3437#include <wtf/Expected.h>
     
    195198}
    196199
     200bool VMInspector::currentThreadOwnsJSLock(ExecState* exec)
     201{
     202    return exec->vm().currentThreadIsHoldingAPILock();
     203}
     204
     205static bool ensureCurrentThreadOwnsJSLock(ExecState* exec)
     206{
     207    if (VMInspector::currentThreadOwnsJSLock(exec))
     208        return true;
     209    dataLog("ERROR: current thread does not own the JSLock\n");
     210    return false;
     211}
     212
     213void VMInspector::gc(ExecState* exec)
     214{
     215    VM& vm = exec->vm();
     216    if (!ensureCurrentThreadOwnsJSLock(exec))
     217        return;
     218    vm.heap.collectNow(Sync, CollectionScope::Full);
     219}
     220
     221void VMInspector::edenGC(ExecState* exec)
     222{
     223    VM& vm = exec->vm();
     224    if (!ensureCurrentThreadOwnsJSLock(exec))
     225        return;
     226    vm.heap.collectSync(CollectionScope::Eden);
     227}
     228
     229bool VMInspector::isInHeap(Heap* heap, void* ptr)
     230{
     231    MarkedBlock* candidate = MarkedBlock::blockFor(ptr);
     232    if (heap->objectSpace().blocks().set().contains(candidate))
     233        return true;
     234    for (LargeAllocation* allocation : heap->objectSpace().largeAllocations()) {
     235        if (allocation->contains(ptr))
     236            return true;
     237    }
     238    return false;
     239}
     240
     241struct CellAddressCheckFunctor : MarkedBlock::CountFunctor {
     242    CellAddressCheckFunctor(JSCell* candidate)
     243        : candidate(candidate)
     244    {
     245    }
     246
     247    IterationStatus operator()(HeapCell* cell, HeapCell::Kind) const
     248    {
     249        if (cell == candidate) {
     250            found = true;
     251            return IterationStatus::Done;
     252        }
     253        return IterationStatus::Continue;
     254    }
     255
     256    JSCell* candidate;
     257    mutable bool found { false };
     258};
     259
     260bool VMInspector::isValidCell(Heap* heap, JSCell* candidate)
     261{
     262    HeapIterationScope iterationScope(*heap);
     263    CellAddressCheckFunctor functor(candidate);
     264    heap->objectSpace().forEachLiveCell(iterationScope, functor);
     265    return functor.found;
     266}
     267
     268bool VMInspector::isValidCodeBlock(ExecState* exec, CodeBlock* candidate)
     269{
     270    if (!ensureCurrentThreadOwnsJSLock(exec))
     271        return false;
     272
     273    struct CodeBlockValidationFunctor {
     274        CodeBlockValidationFunctor(CodeBlock* candidate)
     275            : candidate(candidate)
     276        {
     277        }
     278
     279        bool operator()(CodeBlock* codeBlock) const
     280        {
     281            if (codeBlock == candidate)
     282                found = true;
     283            return found;
     284        }
     285
     286        CodeBlock* candidate;
     287        mutable bool found { false };
     288    };
     289
     290    VM& vm = exec->vm();
     291    CodeBlockValidationFunctor functor(candidate);
     292    vm.heap.forEachCodeBlock(functor);
     293    return functor.found;
     294}
     295
     296CodeBlock* VMInspector::codeBlockForFrame(CallFrame* topCallFrame, unsigned frameNumber)
     297{
     298    if (!ensureCurrentThreadOwnsJSLock(topCallFrame))
     299        return nullptr;
     300
     301    if (!topCallFrame)
     302        return nullptr;
     303
     304    struct FetchCodeBlockFunctor {
     305    public:
     306        FetchCodeBlockFunctor(unsigned targetFrameNumber)
     307            : targetFrame(targetFrameNumber)
     308        {
     309        }
     310
     311        StackVisitor::Status operator()(StackVisitor& visitor) const
     312        {
     313            auto currentFrame = nextFrame++;
     314            if (currentFrame == targetFrame) {
     315                codeBlock = visitor->codeBlock();
     316                return StackVisitor::Done;
     317            }
     318            return StackVisitor::Continue;
     319        }
     320
     321        unsigned targetFrame;
     322        mutable unsigned nextFrame { 0 };
     323        mutable CodeBlock* codeBlock { nullptr };
     324    };
     325
     326    FetchCodeBlockFunctor functor(frameNumber);
     327    topCallFrame->iterate(functor);
     328    return functor.codeBlock;
     329}
     330
     331class PrintFrameFunctor {
     332public:
     333    enum Action {
     334        PrintOne,
     335        PrintAll
     336    };
     337
     338    PrintFrameFunctor(Action action, unsigned framesToSkip)
     339        : m_action(action)
     340        , m_framesToSkip(framesToSkip)
     341    {
     342    }
     343
     344    StackVisitor::Status operator()(StackVisitor& visitor) const
     345    {
     346        m_currentFrame++;
     347        if (m_currentFrame > m_framesToSkip) {
     348            visitor->dump(WTF::dataFile(), Indenter(2), [&] (PrintStream& out) {
     349                out.print("[", (m_currentFrame - m_framesToSkip - 1), "] ");
     350            });
     351        }
     352        if (m_action == PrintOne && m_currentFrame > m_framesToSkip)
     353            return StackVisitor::Done;
     354        return StackVisitor::Continue;
     355    }
     356
     357private:
     358    Action m_action;
     359    unsigned m_framesToSkip;
     360    mutable unsigned m_currentFrame { 0 };
     361};
     362
     363void VMInspector::printCallFrame(CallFrame* callFrame, unsigned framesToSkip)
     364{
     365    if (!ensureCurrentThreadOwnsJSLock(callFrame))
     366        return;
     367    PrintFrameFunctor functor(PrintFrameFunctor::PrintOne, framesToSkip);
     368    callFrame->iterate(functor);
     369}
     370
     371void VMInspector::printStack(CallFrame* topCallFrame, unsigned framesToSkip)
     372{
     373    if (!ensureCurrentThreadOwnsJSLock(topCallFrame))
     374        return;
     375    if (!topCallFrame)
     376        return;
     377    PrintFrameFunctor functor(PrintFrameFunctor::PrintAll, framesToSkip);
     378    topCallFrame->iterate(functor);
     379}
     380
     381void VMInspector::printValue(JSValue value)
     382{
     383    dataLog(value);
     384}
     385
    197386} // namespace JSC
  • trunk/Source/JavaScriptCore/tools/VMInspector.h

    r213652 r224838  
    2626#pragma once
    2727
     28#include "CallFrame.h"
    2829#include "VM.h"
    2930#include <wtf/DoublyLinkedList.h>
     
    6263    Expected<CodeBlock*, Error> codeBlockForMachinePC(const Locker&, void*);
    6364
     65    JS_EXPORT_PRIVATE static bool currentThreadOwnsJSLock(ExecState*);
     66    JS_EXPORT_PRIVATE static void gc(ExecState*);
     67    JS_EXPORT_PRIVATE static void edenGC(ExecState*);
     68    JS_EXPORT_PRIVATE static bool isInHeap(Heap*, void*);
     69    JS_EXPORT_PRIVATE static bool isValidCell(Heap*, JSCell*);
     70    JS_EXPORT_PRIVATE static bool isValidCodeBlock(ExecState*, CodeBlock*);
     71    JS_EXPORT_PRIVATE static CodeBlock* codeBlockForFrame(CallFrame* topCallFrame, unsigned frameNumber);
     72    JS_EXPORT_PRIVATE static void printCallFrame(CallFrame*, unsigned framesToSkip);
     73    JS_EXPORT_PRIVATE static void printStack(CallFrame* topCallFrame, unsigned framesToSkip);
     74    JS_EXPORT_PRIVATE static void printValue(JSValue);
     75
    6476private:
    6577    template <typename Functor> void iterate(const Functor& functor)
Note: See TracChangeset for help on using the changeset viewer.