Changeset 144154 in webkit
- Timestamp:
- Feb 27, 2013 12:37:15 AM (11 years ago)
- Location:
- trunk
- Files:
-
- 12 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/LayoutTests/inspector/timeline/timeline-enum-stability-expected.txt
r144053 r144154 20 20 ParseHTML : "ParseHTML" 21 21 Program : "Program" 22 Rasterize : "Rasterize" 22 23 RecalculateStyles : "RecalculateStyles" 23 24 RequestAnimationFrame : "RequestAnimationFrame" -
trunk/Source/WebCore/ChangeLog
r144146 r144154 1 2013-02-26 Andrey Kosyakov <caseq@chromium.org> 2 3 Web Inspector: show raster tasks on Timeline 4 https://bugs.webkit.org/show_bug.cgi?id=105851 5 6 - add DeferPaint, Paint & RasterTask trace events handling to trace event processor; 7 - upon begin frame, emit aggregated background event for all raster tasks related to inspected page. 8 9 Reviewed by Pavel Feldman. 10 11 * inspector/InspectorInstrumentation.cpp: 12 (WebCore): 13 (WebCore::InspectorInstrumentation::willPaintImpl): 14 (InstrumentationEvents): 15 (InstrumentationEventArguments): 16 * inspector/InspectorInstrumentation.h: 17 (InstrumentationEvents): 18 (WebCore): 19 (InstrumentationEventArguments): 20 * inspector/InspectorTimelineAgent.cpp: 21 (TimelineRecordType): 22 (WebCore::InspectorTimelineAgent::stop): 23 (WebCore::InspectorTimelineAgent::didBeginFrame): 24 * inspector/InspectorTimelineAgent.h: 25 (TimelineRecordType): 26 (WebCore): 27 * inspector/TimelineRecordFactory.cpp: 28 (WebCore::TimelineRecordFactory::createRasterData): 29 (WebCore): 30 * inspector/TimelineRecordFactory.h: 31 (TimelineRecordFactory): 32 * inspector/TimelineTraceEventProcessor.cpp: 33 (WebCore::TimelineTraceEventProcessor::TimelineTraceEventProcessor): 34 (WebCore::TimelineTraceEventProcessor::~TimelineTraceEventProcessor): 35 (WebCore): 36 (WebCore::TimelineTraceEventProcessor::registerHandler): 37 (WebCore::TimelineTraceEventProcessor::shutdown): 38 (WebCore::TimelineTraceEventProcessor::TraceEvent::findParameter): 39 (WebCore::TimelineTraceEventProcessor::TraceEvent::parameter): 40 (WebCore::TimelineTraceEventProcessor::processEventOnAnyThread): 41 (WebCore::TimelineTraceEventProcessor::onBeginFrame): 42 (WebCore::TimelineTraceEventProcessor::onPaintLayerBegin): 43 (WebCore::TimelineTraceEventProcessor::onPaintLayerEnd): 44 (WebCore::TimelineTraceEventProcessor::onRasterTaskBegin): 45 (WebCore::TimelineTraceEventProcessor::onRasterTaskEnd): 46 (WebCore::TimelineTraceEventProcessor::onLayerDeleted): 47 (WebCore::TimelineTraceEventProcessor::onPaint): 48 (WebCore::TimelineTraceEventProcessor::flushRasterizerStatistics): 49 (WebCore::TimelineTraceEventProcessor::sendTimelineRecord): 50 (WebCore::TimelineTraceEventProcessor::processBackgroundEvents): 51 * inspector/TimelineTraceEventProcessor.h: 52 (WebCore::TimelineTraceEventProcessor::TraceEvent::TraceEvent): 53 (WebCore::TimelineTraceEventProcessor::TraceEvent::id): 54 (WebCore::TimelineTraceEventProcessor::TraceEvent::asInt): 55 (WebCore::TimelineTraceEventProcessor::TraceEvent::asUInt): 56 (TimelineTraceEventProcessor): 57 * inspector/front-end/TimelineModel.js: 58 * inspector/front-end/TimelinePresentationModel.js: 59 (WebInspector.TimelinePresentationModel._initRecordStyles): 60 1 61 2013-02-26 Uday Kiran <udaykiran@motorola.com> 2 62 -
trunk/Source/WebCore/inspector/InspectorInstrumentation.cpp
r144053 r144154 79 79 #include <wtf/text/CString.h> 80 80 81 #if PLATFORM(CHROMIUM) 82 #include "platform/chromium/TraceEvent.h" 83 #endif 84 81 85 namespace WebCore { 82 86 … … 524 528 InspectorInstrumentationCookie InspectorInstrumentation::willPaintImpl(InstrumentingAgents* instrumentingAgents, Frame* frame) 525 529 { 530 #if PLATFORM(CHROMIUM) 531 TRACE_EVENT_INSTANT1("instrumentation", InstrumentationEvents::Paint, InstrumentationEventArguments::PageId, frame ? reinterpret_cast<unsigned long long>(frame->page()) : 0); 532 #endif 533 526 534 int timelineAgentId = 0; 527 535 if (InspectorTimelineAgent* timelineAgent = instrumentingAgents->inspectorTimelineAgent()) { … … 1362 1370 #endif 1363 1371 1372 namespace InstrumentationEvents { 1373 const char PaintLayer[] = "PaintLayer"; 1374 const char RasterTask[] = "RasterTask"; 1375 const char Paint[] = "Paint"; 1376 const char Layer[] = "Layer"; 1377 const char BeginFrame[] = "BeginFrame"; 1378 }; 1379 1380 namespace InstrumentationEventArguments { 1381 const char LayerId[] = "layerId"; 1382 const char PageId[] = "pageId"; 1383 }; 1384 1364 1385 } // namespace WebCore 1365 1386 -
trunk/Source/WebCore/inspector/InspectorInstrumentation.h
r144053 r144154 501 501 }; 502 502 503 namespace InstrumentationEvents { 504 extern const char PaintLayer[]; 505 extern const char RasterTask[]; 506 extern const char Paint[]; 507 extern const char Layer[]; 508 extern const char BeginFrame[]; 509 }; 510 511 namespace InstrumentationEventArguments { 512 extern const char LayerId[]; 513 extern const char PageId[]; 514 }; 515 503 516 inline void InspectorInstrumentation::didClearWindowObjectInWorld(Frame* frame, DOMWrapperWorld* world) 504 517 { -
trunk/Source/WebCore/inspector/InspectorTimelineAgent.cpp
r144062 r144154 118 118 static const char WebSocketReceiveHandshakeResponse[] = "WebSocketReceiveHandshakeResponse"; 119 119 static const char WebSocketDestroy[] = "WebSocketDestroy"; 120 121 // Event names visible to other modules. 122 const char Rasterize[] = "Rasterize"; 120 123 } 121 124 … … 190 193 return; 191 194 195 m_traceEventProcessor->shutdown(); 192 196 m_traceEventProcessor.clear(); 193 197 m_weakFactory.revokeAll(); … … 223 227 void InspectorTimelineAgent::didBeginFrame() 224 228 { 229 #if PLATFORM(CHROMIUM) 230 TRACE_EVENT_INSTANT0("webkit", InstrumentationEvents::BeginFrame); 231 #endif 225 232 m_pendingFrameRecord = TimelineRecordFactory::createGenericRecord(timestamp(), 0); 226 233 } -
trunk/Source/WebCore/inspector/InspectorTimelineAgent.h
r144062 r144154 64 64 typedef String ErrorString; 65 65 66 namespace TimelineRecordType { 67 extern const char Rasterize[]; 68 }; 69 66 70 class InspectorTimelineAgent 67 71 : public InspectorBaseAgent<InspectorTimelineAgent>, -
trunk/Source/WebCore/inspector/TimelineRecordFactory.cpp
r144053 r144154 208 208 } 209 209 210 PassRefPtr<InspectorObject> TimelineRecordFactory::createRasterData(double totalCPUTime, int threadsUsed) 211 { 212 RefPtr<InspectorObject> data = InspectorObject::create(); 213 data->setNumber("totalCPUTime", totalCPUTime); 214 data->setNumber("threadsUsed", threadsUsed); 215 return data.release(); 216 } 217 210 218 } // namespace WebCore 211 219 -
trunk/Source/WebCore/inspector/TimelineRecordFactory.h
r144053 r144154 107 107 } 108 108 #endif 109 static PassRefPtr<InspectorObject> createRasterData(double totalCPUTime, int threadsUsed); 109 110 110 111 private: -
trunk/Source/WebCore/inspector/TimelineTraceEventProcessor.cpp
r144062 r144154 76 76 return; 77 77 } 78 m_processors[index] = m_processors.last(); 79 m_processors.removeLast(); 78 m_processors.remove(index); 80 79 if (m_processors.isEmpty()) 81 80 client->setTraceEventCallback(0); … … 110 109 , m_inspectorClient(client) 111 110 , m_pageId(reinterpret_cast<unsigned long long>(m_timelineAgent.get()->page())) 112 { 111 , m_firstRasterStartTime(0) 112 , m_lastRasterEndTime(0) 113 , m_frameRasterTime(0) 114 , m_layerId(0) 115 { 116 registerHandler(InstrumentationEvents::BeginFrame, TracePhaseInstant, &TimelineTraceEventProcessor::onBeginFrame); 117 registerHandler(InstrumentationEvents::PaintLayer, TracePhaseBegin, &TimelineTraceEventProcessor::onPaintLayerBegin); 118 registerHandler(InstrumentationEvents::PaintLayer, TracePhaseEnd, &TimelineTraceEventProcessor::onPaintLayerEnd); 119 registerHandler(InstrumentationEvents::RasterTask, TracePhaseBegin, &TimelineTraceEventProcessor::onRasterTaskBegin); 120 registerHandler(InstrumentationEvents::RasterTask, TracePhaseEnd, &TimelineTraceEventProcessor::onRasterTaskEnd); 121 registerHandler(InstrumentationEvents::Layer, TracePhaseDeleteObject, &TimelineTraceEventProcessor::onLayerDeleted); 122 registerHandler(InstrumentationEvents::Paint, TracePhaseInstant, &TimelineTraceEventProcessor::onPaint); 123 113 124 TraceEventDispatcher::instance()->addProcessor(this, m_inspectorClient); 114 125 } … … 116 127 TimelineTraceEventProcessor::~TimelineTraceEventProcessor() 117 128 { 129 } 130 131 void TimelineTraceEventProcessor::registerHandler(const char* name, TraceEventPhase phase, TraceEventHandler handler) 132 { 133 m_handlersByType.set(std::make_pair(name, phase), handler); 134 } 135 136 void TimelineTraceEventProcessor::shutdown() 137 { 118 138 TraceEventDispatcher::instance()->removeProcessor(this, m_inspectorClient); 119 139 } 120 140 141 size_t TimelineTraceEventProcessor::TraceEvent::findParameter(const char* name) const 142 { 143 for (int i = 0; i < m_argumentCount; ++i) { 144 if (!strcmp(name, m_argumentNames[i])) 145 return i; 146 } 147 return notFound; 148 } 149 121 150 const TimelineTraceEventProcessor::TraceValueUnion& TimelineTraceEventProcessor::TraceEvent::parameter(const char* name, TraceValueTypes expectedType) const 122 151 { 123 152 static TraceValueUnion missingValue; 124 125 for (int i = 0; i < m_argumentCount; ++i) { 126 if (!strcmp(name, m_argumentNames[i])) { 127 if (m_argumentTypes[i] != expectedType) { 128 ASSERT_NOT_REACHED(); 129 return missingValue; 130 } 131 return *reinterpret_cast<const TraceValueUnion*>(m_argumentValues + i); 132 } 133 } 134 ASSERT_NOT_REACHED(); 135 return missingValue; 136 } 137 138 void TimelineTraceEventProcessor::processEventOnAnyThread(TraceEventPhase phase, const char* name, unsigned long long, 153 size_t index = findParameter(name); 154 if (index == notFound || m_argumentTypes[index] != expectedType) { 155 ASSERT_NOT_REACHED(); 156 return missingValue; 157 } 158 return *reinterpret_cast<const TraceValueUnion*>(m_argumentValues + index); 159 } 160 161 void TimelineTraceEventProcessor::processEventOnAnyThread(TraceEventPhase phase, const char* name, unsigned long long id, 139 162 int numArgs, const char* const* argNames, const unsigned char* argTypes, const unsigned long long* argValues, 140 163 unsigned char) 141 164 { 142 Ha shMap<String, EventTypeEntry>::iterator it = m_handlersByType.find(name);165 HandlersMap::iterator it = m_handlersByType.find(std::make_pair(name, phase)); 143 166 if (it == m_handlersByType.end()) 144 167 return; 145 168 146 TraceEvent event(WTF::monotonicallyIncreasingTime(), phase, name, currentThread(), numArgs, argNames, argTypes, argValues);169 TraceEvent event(WTF::monotonicallyIncreasingTime(), phase, name, id, currentThread(), numArgs, argNames, argTypes, argValues); 147 170 148 171 if (!isMainThread()) { … … 151 174 return; 152 175 } 153 154 processEvent(it->value, event); 155 } 156 157 void TimelineTraceEventProcessor::processEvent(const EventTypeEntry& eventTypeEntry, const TraceEvent& event) 158 { 159 TraceEventHandler handler = 0; 160 switch (event.phase()) { 161 case TracePhaseBegin: 162 handler = eventTypeEntry.m_begin; 163 break; 164 case TracePhaseEnd: 165 handler = eventTypeEntry.m_end; 166 break; 167 case TracePhaseInstant: 168 handler = eventTypeEntry.m_instant; 169 break; 170 default: 171 ASSERT_NOT_REACHED(); 172 } 173 if (!handler) { 174 ASSERT_NOT_REACHED(); 175 return; 176 } 177 (this->*handler)(event); 176 (this->*(it->value))(event); 177 } 178 179 void TimelineTraceEventProcessor::onBeginFrame(const TraceEvent&) 180 { 181 flushRasterizerStatistics(); 182 } 183 184 void TimelineTraceEventProcessor::onPaintLayerBegin(const TraceEvent& event) 185 { 186 m_layerId = event.asUInt(InstrumentationEventArguments::LayerId); 187 ASSERT(m_layerId); 188 } 189 190 void TimelineTraceEventProcessor::onPaintLayerEnd(const TraceEvent&) 191 { 192 m_layerId = 0; 193 } 194 195 void TimelineTraceEventProcessor::onRasterTaskBegin(const TraceEvent& event) 196 { 197 unsigned long long layerId = event.asUInt(InstrumentationEventArguments::LayerId); 198 ThreadIdentifier threadIdentifier = event.threadIdentifier(); 199 ASSERT(m_rasterStartTimeByThread.get(threadIdentifier) == HashTraits<double>::emptyValue()); 200 double timestamp = m_knownLayers.contains(layerId) ? event.timestamp() : 0; 201 m_rasterStartTimeByThread.set(threadIdentifier, timestamp); 202 } 203 204 void TimelineTraceEventProcessor::onRasterTaskEnd(const TraceEvent& event) 205 { 206 HashMap<ThreadIdentifier, double>::iterator it = m_rasterStartTimeByThread.find(event.threadIdentifier()); 207 if (it == m_rasterStartTimeByThread.end()) 208 return; 209 double startTime = it->value; 210 double endTime = event.timestamp(); 211 if (startTime == HashTraits<double>::emptyValue()) // Rasterizing unknown layer. 212 return; 213 m_frameRasterTime += endTime - startTime; 214 it->value = HashTraits<double>::emptyValue(); 215 if (!m_firstRasterStartTime || m_firstRasterStartTime > startTime) 216 m_firstRasterStartTime = startTime; 217 m_lastRasterEndTime = endTime; 218 } 219 220 void TimelineTraceEventProcessor::onLayerDeleted(const TraceEvent& event) 221 { 222 unsigned long long id = event.id(); 223 ASSERT(id); 224 processBackgroundEvents(); 225 m_knownLayers.remove(id); 226 } 227 228 void TimelineTraceEventProcessor::onPaint(const TraceEvent& event) 229 { 230 if (!m_layerId) 231 return; 232 233 unsigned long long pageId = event.asUInt(InstrumentationEventArguments::PageId); 234 if (pageId == m_pageId) 235 m_knownLayers.add(m_layerId); 236 } 237 238 void TimelineTraceEventProcessor::flushRasterizerStatistics() 239 { 240 processBackgroundEvents(); 241 if (m_lastRasterEndTime) { 242 RefPtr<InspectorObject> data = TimelineRecordFactory::createRasterData(m_frameRasterTime, m_rasterStartTimeByThread.size()); 243 sendTimelineRecord(data, TimelineRecordType::Rasterize, m_firstRasterStartTime, m_lastRasterEndTime, "multiple"); 244 } 245 m_firstRasterStartTime = 0; 246 m_lastRasterEndTime = 0; 247 m_frameRasterTime = 0; 178 248 } 179 249 180 250 void TimelineTraceEventProcessor::sendTimelineRecord(PassRefPtr<InspectorObject> data, const String& recordType, double startTime, double endTime, const String& thread) 181 251 { 252 ASSERT(isMainThread()); 182 253 InspectorTimelineAgent* timelineAgent = m_timelineAgent.get(); 183 254 if (!timelineAgent) … … 188 259 void TimelineTraceEventProcessor::processBackgroundEvents() 189 260 { 261 ASSERT(isMainThread()); 190 262 Vector<TraceEvent> events; 191 263 { … … 196 268 for (size_t i = 0, size = events.size(); i < size; ++i) { 197 269 const TraceEvent& event = events[i]; 198 processEvent(m_handlersByType.find(event.name())->value, event); 270 HandlersMap::iterator it = m_handlersByType.find(std::make_pair(event.name(), event.phase())); 271 ASSERT(it != m_handlersByType.end() && it->value); 272 (this->*(it->value))(event); 199 273 } 200 274 } -
trunk/Source/WebCore/inspector/TimelineTraceEventProcessor.h
r144062 r144154 56 56 TracePhaseBegin = 'B', 57 57 TracePhaseEnd = 'E', 58 TracePhaseInstant = 'I' 58 TracePhaseInstant = 'I', 59 TracePhaseCreateObject = 'N', 60 TracePhaseDeleteObject = 'D' 59 61 }; 60 62 … … 62 64 ~TimelineTraceEventProcessor(); 63 65 66 void shutdown(); 64 67 void processEventOnAnyThread(TraceEventPhase, const char* name, unsigned long long id, 65 68 int numArgs, const char* const* argNames, const unsigned char* argTypes, const unsigned long long* argValues, … … 94 97 } 95 98 96 TraceEvent(double timestamp, TraceEventPhase phase, const char* name, ThreadIdentifier threadIdentifier,99 TraceEvent(double timestamp, TraceEventPhase phase, const char* name, unsigned long long id, ThreadIdentifier threadIdentifier, 97 100 int argumentCount, const char* const* argumentNames, const unsigned char* argumentTypes, const unsigned long long* argumentValues) 98 101 : m_timestamp(timestamp) 99 102 , m_phase(phase) 100 103 , m_name(name) 104 , m_id(id) 101 105 , m_threadIdentifier(threadIdentifier) 102 106 , m_argumentCount(argumentCount) 103 , m_argumentNames(argumentNames) 104 , m_argumentTypes(argumentTypes) 105 , m_argumentValues(argumentValues) 106 { 107 { 108 if (m_argumentCount > MaxArguments) { 109 ASSERT_NOT_REACHED(); 110 m_argumentCount = MaxArguments; 111 } 112 for (int i = 0; i < m_argumentCount; ++i) { 113 m_argumentNames[i] = argumentNames[i]; 114 m_argumentTypes[i] = argumentTypes[i]; 115 m_argumentValues[i] = argumentValues[i]; 116 } 107 117 } 108 118 … … 110 120 TraceEventPhase phase() const { return m_phase; } 111 121 const char* name() const { return m_name; } 122 unsigned long long id() const { return m_id; } 112 123 ThreadIdentifier threadIdentifier() const { return m_threadIdentifier; } 113 124 int argumentCount() const { return m_argumentCount; } … … 119 130 long long asInt(const char* name) const 120 131 { 121 return parameter(name, TypeInt).m_int; 132 size_t index = findParameter(name); 133 if (index == notFound || (m_argumentTypes[index] != TypeInt && m_argumentTypes[index] != TypeUInt)) { 134 ASSERT_NOT_REACHED(); 135 return 0; 136 } 137 return reinterpret_cast<const TraceValueUnion*>(m_argumentValues + index)->m_int; 122 138 } 123 139 unsigned long long asUInt(const char* name) const 124 140 { 125 return parameter(name, TypeUInt).m_uint;141 return asInt(name); 126 142 } 127 143 double asDouble(const char* name) const … … 135 151 136 152 private: 153 enum { MaxArguments = 2 }; 154 155 size_t findParameter(const char*) const; 137 156 const TraceValueUnion& parameter(const char* name, TraceValueTypes expectedType) const; 138 157 … … 140 159 TraceEventPhase m_phase; 141 160 const char* m_name; 161 unsigned long long m_id; 142 162 ThreadIdentifier m_threadIdentifier; 143 163 int m_argumentCount; 144 const char* const* m_argumentNames;145 const unsigned char* m_argumentTypes;146 const unsigned long long* m_argumentValues;164 const char* m_argumentNames[MaxArguments]; 165 unsigned char m_argumentTypes[MaxArguments]; 166 unsigned long long m_argumentValues[MaxArguments]; 147 167 }; 148 168 149 169 typedef void (TimelineTraceEventProcessor::*TraceEventHandler)(const TraceEvent&); 150 170 151 struct EventTypeEntry {152 EventTypeEntry()153 : m_begin(0)154 , m_end(0)155 , m_instant(0)156 {157 }158 explicit EventTypeEntry(TraceEventHandler instant)159 : m_begin(0)160 , m_end(0)161 , m_instant(instant)162 {163 }164 EventTypeEntry(TraceEventHandler begin, TraceEventHandler end)165 : m_begin(begin)166 , m_end(end)167 , m_instant(0)168 {169 }170 171 TraceEventHandler m_begin;172 TraceEventHandler m_end;173 TraceEventHandler m_instant;174 };175 176 171 void processBackgroundEvents(); 177 void sendTimelineRecord(PassRefPtr<InspectorObject> data, const String& recordType, double startTime, double endTime, const String& thread); 178 void processEvent(const EventTypeEntry&, const TraceEvent&); 172 void sendTimelineRecord(PassRefPtr<InspectorObject> data, const String& recordType, double startTime, double endTime, const String& Thread); 173 174 void onBeginFrame(const TraceEvent&); 175 void onPaintLayerBegin(const TraceEvent&); 176 void onPaintLayerEnd(const TraceEvent&); 177 void onRasterTaskBegin(const TraceEvent&); 178 void onRasterTaskEnd(const TraceEvent&); 179 void onLayerDeleted(const TraceEvent&); 180 void onPaint(const TraceEvent&); 181 182 void flushRasterizerStatistics(); 183 184 void registerHandler(const char* name, TraceEventPhase, TraceEventHandler); 179 185 180 186 WeakPtr<InspectorTimelineAgent> m_timelineAgent; 181 187 InspectorClient* m_inspectorClient; 182 188 183 HashMap<String, EventTypeEntry> m_handlersByType; 189 typedef HashMap<std::pair<String, int>, TraceEventHandler> HandlersMap; 190 HandlersMap m_handlersByType; 184 191 Mutex m_backgroundEventsMutex; 185 192 Vector<TraceEvent> m_backgroundEvents; 186 193 unsigned long long m_pageId; 194 195 HashSet<unsigned long long> m_knownLayers; 196 HashMap<ThreadIdentifier, double> m_rasterStartTimeByThread; 197 double m_firstRasterStartTime; 198 double m_lastRasterEndTime; 199 double m_frameRasterTime; 200 201 unsigned long long m_layerId; 187 202 }; 188 189 203 190 204 } // namespace WebCore -
trunk/Source/WebCore/inspector/front-end/TimelineModel.js
r144053 r144154 57 57 Layout: "Layout", 58 58 Paint: "Paint", 59 Rasterize: "Rasterize", 59 60 ScrollLayer: "ScrollLayer", 60 61 DecodeImage: "DecodeImage", -
trunk/Source/WebCore/inspector/front-end/TimelinePresentationModel.js
r144053 r144154 77 77 recordStyles[recordTypes.Layout] = { title: WebInspector.UIString("Layout"), category: categories["rendering"] }; 78 78 recordStyles[recordTypes.Paint] = { title: WebInspector.UIString("Paint"), category: categories["painting"] }; 79 recordStyles[recordTypes.Rasterize] = { title: WebInspector.UIString("Rasterize"), category: categories["painting"] }; 79 80 recordStyles[recordTypes.ScrollLayer] = { title: WebInspector.UIString("Scroll"), category: categories["painting"] }; 80 81 recordStyles[recordTypes.DecodeImage] = { title: WebInspector.UIString("Image Decode"), category: categories["painting"] };
Note: See TracChangeset
for help on using the changeset viewer.