From 9d9f608a382e45364edcf4db2bd41073582cfb07 Mon Sep 17 00:00:00 2001 From: "christian.plesner.hansen@gmail.com" Date: Fri, 24 Oct 2008 08:40:02 +0000 Subject: [PATCH] - Added caching of regexp data in the compilation cache. - Changed the structure of regexp objects from having two internal fields to having a single field containing a fixed array, since it's easier to store the whole fixed array in the cache. - Move printing of the command to after printing std{err,out} in the compact progress indicators in the test framework. git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@579 ce2b1a6d-e550-0410-aec6-3dcde31c8c00 --- src/compilation-cache.cc | 31 ++++++++++-- src/compilation-cache.h | 21 ++++++-- src/compiler.cc | 4 +- src/factory.cc | 14 ++++++ src/factory.h | 8 ++++ src/jsregexp.cc | 101 +++++++++++++++++++++++++-------------- src/jsregexp.h | 5 +- src/log.cc | 8 ++-- src/log.h | 2 +- src/objects-debug.cc | 27 ++++++----- src/objects-inl.h | 13 +++-- src/objects.cc | 66 +++++++++++++++++++++++++ src/objects.h | 60 +++++++++++++++-------- src/v8-counters.h | 2 + tools/test.py | 2 +- 15 files changed, 271 insertions(+), 93 deletions(-) diff --git a/src/compilation-cache.cc b/src/compilation-cache.cc index 48b287022..bf3262124 100644 --- a/src/compilation-cache.cc +++ b/src/compilation-cache.cc @@ -32,7 +32,7 @@ namespace v8 { namespace internal { enum { - NUMBER_OF_ENTRY_KINDS = CompilationCache::EVAL_CONTEXTUAL + 1 + NUMBER_OF_ENTRY_KINDS = CompilationCache::LAST_ENTRY + 1 }; @@ -132,9 +132,9 @@ Handle CompilationCache::LookupEval(Handle source, } -void CompilationCache::Associate(Handle source, - Entry entry, - Handle boilerplate) { +void CompilationCache::PutFunction(Handle source, + Entry entry, + Handle boilerplate) { HandleScope scope; ASSERT(boilerplate->IsBoilerplate()); Handle table = GetTable(entry); @@ -142,6 +142,29 @@ void CompilationCache::Associate(Handle source, } +Handle CompilationCache::LookupRegExp(Handle source, + JSRegExp::Flags flags) { + Handle table = GetTable(REGEXP); + Object* result = table->LookupRegExp(*source, flags); + if (result->IsFixedArray()) { + Counters::regexp_cache_hits.Increment(); + return Handle(FixedArray::cast(result)); + } else { + Counters::regexp_cache_misses.Increment(); + return Handle(); + } +} + + +void CompilationCache::PutRegExp(Handle source, + JSRegExp::Flags flags, + Handle data) { + HandleScope scope; + Handle table = GetTable(REGEXP); + CALL_HEAP_FUNCTION_VOID(table->PutRegExp(*source, flags, *data)); +} + + void CompilationCache::Clear() { for (int i = 0; i < NUMBER_OF_ENTRY_KINDS; i++) { tables[i] = Heap::undefined_value(); diff --git a/src/compilation-cache.h b/src/compilation-cache.h index a87c4958c..abe61102c 100644 --- a/src/compilation-cache.h +++ b/src/compilation-cache.h @@ -42,7 +42,9 @@ class CompilationCache { enum Entry { SCRIPT, EVAL_GLOBAL, - EVAL_CONTEXTUAL + EVAL_CONTEXTUAL, + REGEXP, + LAST_ENTRY = REGEXP }; // Finds the script function boilerplate for a source @@ -59,11 +61,22 @@ class CompilationCache { static Handle LookupEval(Handle source, Entry entry); + // Returns the regexp data associated with the given regexp if it + // is in cache, otherwise an empty handle. + static Handle LookupRegExp(Handle source, + JSRegExp::Flags flags); + + // Associate the (source, flags) pair to the given regexp data. + // This may overwrite an existing mapping. + static void PutRegExp(Handle source, + JSRegExp::Flags flags, + Handle data); + // Associate the (source, kind) pair to the boilerplate. This may // overwrite an existing mapping. - static void Associate(Handle source, - Entry entry, - Handle boilerplate); + static void PutFunction(Handle source, + Entry entry, + Handle boilerplate); // Clear the cache - also used to initialize the cache at startup. static void Clear(); diff --git a/src/compiler.cc b/src/compiler.cc index 999cf4c59..11ac1457e 100644 --- a/src/compiler.cc +++ b/src/compiler.cc @@ -189,7 +189,7 @@ Handle Compiler::Compile(Handle source, // Compile the function and add it to the cache. result = MakeFunction(true, false, script, extension, pre_data); if (extension == NULL && !result.is_null()) { - CompilationCache::Associate(source, CompilationCache::SCRIPT, result); + CompilationCache::PutFunction(source, CompilationCache::SCRIPT, result); } // Get rid of the pre-parsing data (if necessary). @@ -223,7 +223,7 @@ Handle Compiler::CompileEval(Handle source, script->set_line_offset(Smi::FromInt(line_offset)); result = MakeFunction(is_global, true, script, NULL, NULL); if (!result.is_null()) { - CompilationCache::Associate(source, entry, result); + CompilationCache::PutFunction(source, entry, result); } } return result; diff --git a/src/factory.cc b/src/factory.cc index 37d69a28c..82e646f92 100644 --- a/src/factory.cc +++ b/src/factory.cc @@ -808,6 +808,20 @@ Handle Factory::ObjectLiteralMapFromCache(Handle context, } +void Factory::SetRegExpData(Handle regexp, + JSRegExp::Type type, + Handle source, + JSRegExp::Flags flags, + Handle data) { + Handle store = NewFixedArray(JSRegExp::kDataSize); + store->set(JSRegExp::kTagIndex, Smi::FromInt(type)); + store->set(JSRegExp::kSourceIndex, *source); + store->set(JSRegExp::kFlagsIndex, Smi::FromInt(flags.value())); + store->set(JSRegExp::kAtomPatternIndex, *data); + regexp->set_data(*store); +} + + void Factory::ConfigureInstance(Handle desc, Handle instance, bool* pending_exception) { diff --git a/src/factory.h b/src/factory.h index 42de71c36..bb04b6b10 100644 --- a/src/factory.h +++ b/src/factory.h @@ -308,6 +308,14 @@ class Factory : public AllStatic { static Handle ObjectLiteralMapFromCache(Handle context, Handle keys); + // Creates a new FixedArray that holds the data associated with the + // regexp and stores it in the regexp. + static void SetRegExpData(Handle regexp, + JSRegExp::Type type, + Handle source, + JSRegExp::Flags flags, + Handle data); + private: static Handle NewFunctionHelper(Handle name, Handle prototype); diff --git a/src/jsregexp.cc b/src/jsregexp.cc index 675f82ac1..de03b6e21 100644 --- a/src/jsregexp.cc +++ b/src/jsregexp.cc @@ -34,6 +34,7 @@ #include "platform.h" #include "runtime.h" #include "top.h" +#include "compilation-cache.h" namespace v8 { namespace internal { @@ -143,29 +144,59 @@ Handle RegExpImpl::StringToTwoByte(Handle pattern) { } +static JSRegExp::Flags RegExpFlagsFromString(Handle str) { + int flags = JSRegExp::NONE; + for (int i = 0; i < str->length(); i++) { + switch (str->Get(i)) { + case 'i': + flags |= JSRegExp::IGNORE_CASE; + break; + case 'g': + flags |= JSRegExp::GLOBAL; + break; + case 'm': + flags |= JSRegExp::MULTILINE; + break; + } + } + return JSRegExp::Flags(flags); +} + + unibrow::Predicate is_reg_exp_special_char; Handle RegExpImpl::Compile(Handle re, Handle pattern, - Handle flags) { - bool is_atom = true; - for (int i = 0; is_atom && i < flags->length(); i++) { - if (flags->Get(i) == 'i') - is_atom = false; - } - for (int i = 0; is_atom && i < pattern->length(); i++) { - if (is_reg_exp_special_char.get(pattern->Get(i))) - is_atom = false; - } + Handle flag_str) { + JSRegExp::Flags flags = RegExpFlagsFromString(flag_str); + Handle cached = CompilationCache::LookupRegExp(pattern, flags); + bool in_cache = !cached.is_null(); Handle result; - if (is_atom) { - result = AtomCompile(re, pattern); + if (in_cache) { + re->set_data(*cached); + result = re; } else { - result = JsreCompile(re, pattern, flags); + bool is_atom = !flags.is_ignore_case(); + for (int i = 0; is_atom && i < pattern->length(); i++) { + if (is_reg_exp_special_char.get(pattern->Get(i))) + is_atom = false; + } + if (is_atom) { + result = AtomCompile(re, pattern, flags); + } else { + result = JsreCompile(re, pattern, flags); + } + Object* data = re->data(); + if (data->IsFixedArray()) { + // If compilation succeeded then the data is set on the regexp + // and we can store it in the cache. + Handle data(FixedArray::cast(re->data())); + CompilationCache::PutRegExp(pattern, flags, data); + } } - LOG(RegExpCompileEvent(re)); + LOG(RegExpCompileEvent(re, in_cache)); return result; } @@ -173,7 +204,7 @@ Handle RegExpImpl::Compile(Handle re, Handle RegExpImpl::Exec(Handle regexp, Handle subject, Handle index) { - switch (regexp->type_tag()) { + switch (regexp->TypeTag()) { case JSRegExp::JSCRE: return JsreExec(regexp, subject, index); case JSRegExp::ATOM: @@ -187,7 +218,7 @@ Handle RegExpImpl::Exec(Handle regexp, Handle RegExpImpl::ExecGlobal(Handle regexp, Handle subject) { - switch (regexp->type_tag()) { + switch (regexp->TypeTag()) { case JSRegExp::JSCRE: return JsreExecGlobal(regexp, subject); case JSRegExp::ATOM: @@ -200,9 +231,9 @@ Handle RegExpImpl::ExecGlobal(Handle regexp, Handle RegExpImpl::AtomCompile(Handle re, - Handle pattern) { - re->set_type_tag(JSRegExp::ATOM); - re->set_data(*pattern); + Handle pattern, + JSRegExp::Flags flags) { + Factory::SetRegExpData(re, JSRegExp::ATOM, pattern, flags, pattern); return re; } @@ -210,7 +241,7 @@ Handle RegExpImpl::AtomCompile(Handle re, Handle RegExpImpl::AtomExec(Handle re, Handle subject, Handle index) { - Handle needle(String::cast(re->data())); + Handle needle(String::cast(re->DataAt(JSRegExp::kAtomPatternIndex))); uint32_t start_index; if (!Array::IndexFromObject(*index, &start_index)) { @@ -234,7 +265,7 @@ Handle RegExpImpl::AtomExec(Handle re, Handle RegExpImpl::AtomExecGlobal(Handle re, Handle subject) { - Handle needle(String::cast(re->data())); + Handle needle(String::cast(re->DataAt(JSRegExp::kAtomPatternIndex))); Handle result = Factory::NewJSArray(1); int index = 0; int match_count = 0; @@ -269,14 +300,13 @@ Handle RegExpImpl::AtomExecGlobal(Handle re, Handle RegExpImpl::JsreCompile(Handle re, Handle pattern, - Handle flags) { - JSRegExpIgnoreCaseOption case_option = JSRegExpDoNotIgnoreCase; - JSRegExpMultilineOption multiline_option = JSRegExpSingleLine; - FlattenString(flags); - for (int i = 0; i < flags->length(); i++) { - if (flags->Get(i) == 'i') case_option = JSRegExpIgnoreCase; - if (flags->Get(i) == 'm') multiline_option = JSRegExpMultiline; - } + JSRegExp::Flags flags) { + JSRegExpIgnoreCaseOption case_option = flags.is_ignore_case() + ? JSRegExpIgnoreCase + : JSRegExpDoNotIgnoreCase; + JSRegExpMultilineOption multiline_option = flags.is_multiline() + ? JSRegExpMultiline + : JSRegExpSingleLine; Handle two_byte_pattern = StringToTwoByte(pattern); @@ -328,8 +358,7 @@ Handle RegExpImpl::JsreCompile(Handle re, Handle value = Factory::NewFixedArray(2); value->set(CAPTURE_INDEX, Smi::FromInt(number_of_captures)); value->set(INTERNAL_INDEX, *internal); - re->set_type_tag(JSRegExp::JSCRE); - re->set_data(*value); + Factory::SetRegExpData(re, JSRegExp::JSCRE, pattern, flags, value); return re; } @@ -499,16 +528,14 @@ Handle RegExpImpl::JsreExecGlobal(Handle regexp, int RegExpImpl::JsreCapture(Handle re) { - Object* value = re->data(); - ASSERT(value->IsFixedArray()); - return Smi::cast(FixedArray::cast(value)->get(CAPTURE_INDEX))->value(); + FixedArray* value = FixedArray::cast(re->DataAt(JSRegExp::kJscreDataIndex)); + return Smi::cast(value->get(CAPTURE_INDEX))->value(); } ByteArray* RegExpImpl::JsreInternal(Handle re) { - Object* value = re->data(); - ASSERT(value->IsFixedArray()); - return ByteArray::cast(FixedArray::cast(value)->get(INTERNAL_INDEX)); + FixedArray* value = FixedArray::cast(re->DataAt(JSRegExp::kJscreDataIndex)); + return ByteArray::cast(value->get(INTERNAL_INDEX)); } }} // namespace v8::internal diff --git a/src/jsregexp.h b/src/jsregexp.h index 75d5c91d4..c05380d79 100644 --- a/src/jsregexp.h +++ b/src/jsregexp.h @@ -62,7 +62,8 @@ class RegExpImpl { Handle subject); static Handle AtomCompile(Handle re, - Handle pattern); + Handle pattern, + JSRegExp::Flags flags); static Handle AtomExec(Handle regexp, Handle subject, @@ -73,7 +74,7 @@ class RegExpImpl { static Handle JsreCompile(Handle re, Handle pattern, - Handle flags); + JSRegExp::Flags flags); static Handle JsreExec(Handle regexp, Handle subject, diff --git a/src/log.cc b/src/log.cc index be76b96e1..796b98ed3 100644 --- a/src/log.cc +++ b/src/log.cc @@ -377,14 +377,12 @@ void Logger::LogRegExpSource(Handle regexp) { return; } - if (regexp->type()->IsSmi()) { - switch (regexp->type_tag()) { + switch (regexp->TypeTag()) { case JSRegExp::ATOM: fprintf(logfile_, "a"); break; default: break; - } } fprintf(logfile_, "/"); LogString(Handle::cast(source)); @@ -409,14 +407,14 @@ void Logger::LogRegExpSource(Handle regexp) { #endif // ENABLE_LOGGING_AND_PROFILING -void Logger::RegExpCompileEvent(Handle regexp) { +void Logger::RegExpCompileEvent(Handle regexp, bool in_cache) { #ifdef ENABLE_LOGGING_AND_PROFILING if (logfile_ == NULL || !FLAG_log_regexp) return; ScopedLock sl(mutex_); fprintf(logfile_, "regexp-compile,"); LogRegExpSource(regexp); - fprintf(logfile_, "\n"); + fprintf(logfile_, in_cache ? ",hit\n" : ",miss\n"); #endif } diff --git a/src/log.h b/src/log.h index 69be1038f..c950b8658 100644 --- a/src/log.h +++ b/src/log.h @@ -181,7 +181,7 @@ class Logger { // ==== Events logged by --log-regexp ==== // Regexp compilation and execution events. - static void RegExpCompileEvent(Handle regexp); + static void RegExpCompileEvent(Handle regexp, bool in_cache); static void RegExpExecEvent(Handle regexp, int start_index, diff --git a/src/objects-debug.cc b/src/objects-debug.cc index e13d17778..d67544e7b 100644 --- a/src/objects-debug.cc +++ b/src/objects-debug.cc @@ -660,19 +660,22 @@ void JSArray::JSArrayVerify() { void JSRegExp::JSRegExpVerify() { JSObjectVerify(); - ASSERT(type()->IsSmi() || type()->IsUndefined()); - if (type()->IsSmi()) { - switch (type_tag()) { - case JSRegExp::JSCRE: - ASSERT(data()->IsFixedArray()); - break; - default: - ASSERT_EQ(JSRegExp::ATOM, type_tag()); - ASSERT(data()->IsString()); - break; + ASSERT(data()->IsUndefined() || data()->IsFixedArray()); + switch (TypeTag()) { + case JSRegExp::ATOM: { + FixedArray* arr = FixedArray::cast(data()); + ASSERT(arr->get(JSRegExp::kAtomPatternIndex)->IsString()); + break; } - } else { - ASSERT(data()->IsUndefined()); + case JSRegExp::JSCRE: { + FixedArray* arr = FixedArray::cast(data()); + ASSERT(arr->get(JSRegExp::kJscreDataIndex)->IsFixedArray()); + break; + } + default: + ASSERT_EQ(JSRegExp::NOT_COMPILED, TypeTag()); + ASSERT(data()->IsUndefined()); + break; } } diff --git a/src/objects-inl.h b/src/objects-inl.h index fe470c898..49fb76a4b 100644 --- a/src/objects-inl.h +++ b/src/objects-inl.h @@ -2166,16 +2166,19 @@ ACCESSORS(JSArray, length, Object, kLengthOffset) ACCESSORS(JSRegExp, data, Object, kDataOffset) -ACCESSORS(JSRegExp, type, Object, kTypeOffset) -JSRegExp::Type JSRegExp::type_tag() { - return static_cast(Smi::cast(type())->value()); +JSRegExp::Type JSRegExp::TypeTag() { + Object* data = this->data(); + if (data->IsUndefined()) return JSRegExp::NOT_COMPILED; + Smi* smi = Smi::cast(FixedArray::cast(data)->get(kTagIndex)); + return static_cast(smi->value()); } -void JSRegExp::set_type_tag(JSRegExp::Type value) { - set_type(Smi::FromInt(value)); +Object* JSRegExp::DataAt(int index) { + ASSERT(TypeTag() != NOT_COMPILED); + return FixedArray::cast(data())->get(index); } diff --git a/src/objects.cc b/src/objects.cc index 65fa9eedc..5be631b43 100644 --- a/src/objects.cc +++ b/src/objects.cc @@ -5551,6 +5551,46 @@ class StringKey : public HashTableKey { String* string_; }; +// RegExpKey carries the source and flags of a regular expression as key. +class RegExpKey : public HashTableKey { + public: + RegExpKey(String* string, JSRegExp::Flags flags) + : string_(string), + flags_(Smi::FromInt(flags.value())) { } + + bool IsMatch(Object* obj) { + FixedArray* val = FixedArray::cast(obj); + return string_->Equals(String::cast(val->get(JSRegExp::kSourceIndex))) + && (flags_ == val->get(JSRegExp::kFlagsIndex)); + } + + uint32_t Hash() { return RegExpHash(string_, flags_); } + + HashFunction GetHashFunction() { return RegExpObjectHash; } + + Object* GetObject() { + // Plain hash maps, which is where regexp keys are used, don't + // use this function. + UNREACHABLE(); + return NULL; + } + + static uint32_t RegExpObjectHash(Object* obj) { + FixedArray* val = FixedArray::cast(obj); + return RegExpHash(String::cast(val->get(JSRegExp::kSourceIndex)), + Smi::cast(val->get(JSRegExp::kFlagsIndex))); + } + + static uint32_t RegExpHash(String* string, Smi* flags) { + return string->Hash() + flags->value(); + } + + bool IsStringKey() { return false; } + + String* string_; + Smi* flags_; +}; + // Utf8SymbolKey carries a vector of chars as key. class Utf8SymbolKey : public HashTableKey { public: @@ -5825,6 +5865,15 @@ Object* CompilationCacheTable::Lookup(String* src) { } +Object* CompilationCacheTable::LookupRegExp(String* src, + JSRegExp::Flags flags) { + RegExpKey key(src, flags); + int entry = FindEntry(&key); + if (entry == -1) return Heap::undefined_value(); + return get(EntryToIndex(entry) + 1); +} + + Object* CompilationCacheTable::Put(String* src, Object* value) { StringKey key(src); Object* obj = EnsureCapacity(1, &key); @@ -5840,6 +5889,23 @@ Object* CompilationCacheTable::Put(String* src, Object* value) { } +Object* CompilationCacheTable::PutRegExp(String* src, + JSRegExp::Flags flags, + FixedArray* value) { + RegExpKey key(src, flags); + Object* obj = EnsureCapacity(1, &key); + if (obj->IsFailure()) return obj; + + CompilationCacheTable* cache = + reinterpret_cast(obj); + int entry = cache->FindInsertionEntry(value, key.Hash()); + cache->set(EntryToIndex(entry), value); + cache->set(EntryToIndex(entry) + 1, value); + cache->ElementAdded(); + return cache; +} + + // SymbolsKey used for HashTable where key is array of symbols. class SymbolsKey : public HashTableKey { public: diff --git a/src/objects.h b/src/objects.h index bf0a3f21d..05379ca6c 100644 --- a/src/objects.h +++ b/src/objects.h @@ -1856,19 +1856,6 @@ class SymbolTable: public HashTable<0, 1> { }; -class CompilationCacheTable: public HashTable<0, 2> { - public: - // Find cached value for a string key, otherwise return null. - Object* Lookup(String* src); - Object* Put(String* src, Object* value); - - static inline CompilationCacheTable* cast(Object* obj); - - private: - DISALLOW_IMPLICIT_CONSTRUCTORS(CompilationCacheTable); -}; - - // MapCache. // // Maps keys that are a fixed array of symbols to a map. @@ -2907,18 +2894,28 @@ class JSValue: public JSObject { DISALLOW_IMPLICIT_CONSTRUCTORS(JSValue); }; - // Regular expressions class JSRegExp: public JSObject { public: - enum Type { JSCRE, ATOM }; + enum Type { NOT_COMPILED, JSCRE, ATOM }; + enum Flag { NONE = 0, GLOBAL = 1, IGNORE_CASE = 2, MULTILINE = 4 }; - inline Type type_tag(); - inline void set_type_tag(Type value); + class Flags { + public: + explicit Flags(uint32_t value) : value_(value) { } + bool is_global() { return (value_ & GLOBAL) != 0; } + bool is_ignore_case() { return (value_ & IGNORE_CASE) != 0; } + bool is_multiline() { return (value_ & MULTILINE) != 0; } + uint32_t value() { return value_; } + private: + uint32_t value_; + }; - DECL_ACCESSORS(type, Object) DECL_ACCESSORS(data, Object) + inline Type TypeTag(); + inline Object* DataAt(int index); + static inline JSRegExp* cast(Object* obj); // Dispatched behavior. @@ -2927,9 +2924,32 @@ class JSRegExp: public JSObject { void JSRegExpVerify(); #endif - static const int kTypeOffset = JSObject::kHeaderSize; - static const int kDataOffset = kTypeOffset + kIntSize; + static const int kDataOffset = JSObject::kHeaderSize; static const int kSize = kDataOffset + kIntSize; + + static const int kTagIndex = 0; + static const int kSourceIndex = kTagIndex + 1; + static const int kFlagsIndex = kSourceIndex + 1; + // These two are the same since the same entry is shared for + // different purposes in different types of regexps. + static const int kAtomPatternIndex = kFlagsIndex + 1; + static const int kJscreDataIndex = kFlagsIndex + 1; + static const int kDataSize = kAtomPatternIndex + 1; +}; + + +class CompilationCacheTable: public HashTable<0, 2> { + public: + // Find cached value for a string key, otherwise return null. + Object* Lookup(String* src); + Object* LookupRegExp(String* source, JSRegExp::Flags flags); + Object* Put(String* src, Object* value); + Object* PutRegExp(String* src, JSRegExp::Flags flags, FixedArray* value); + + static inline CompilationCacheTable* cast(Object* obj); + + private: + DISALLOW_IMPLICIT_CONSTRUCTORS(CompilationCacheTable); }; diff --git a/src/v8-counters.h b/src/v8-counters.h index 9819cdc69..6eb7426b7 100644 --- a/src/v8-counters.h +++ b/src/v8-counters.h @@ -73,6 +73,8 @@ namespace v8 { namespace internal { SC(arguments_adaptors, V8.ArgumentsAdaptors) \ SC(compilation_cache_hits, V8.CompilationCacheHits) \ SC(compilation_cache_misses, V8.CompilationCacheMisses) \ + SC(regexp_cache_hits, V8.RegExpCacheHits) \ + SC(regexp_cache_misses, V8.RegExpCacheMisses) \ /* Amount of evaled source code. */ \ SC(total_eval_size, V8.TotalEvalSize) \ /* Amount of loaded source code. */ \ diff --git a/tools/test.py b/tools/test.py index cbd76bd03..30f0f349c 100755 --- a/tools/test.py +++ b/tools/test.py @@ -222,13 +222,13 @@ class CompactProgressIndicator(ProgressIndicator): if output.UnexpectedOutput(): self.ClearLine(self.last_status_length) self.PrintFailureHeader(output.test) - print "Command: %s" % EscapeCommand(output.command) stdout = output.output.stdout.strip() if len(stdout): print self.templates['stdout'] % stdout stderr = output.output.stderr.strip() if len(stderr): print self.templates['stderr'] % stderr + print "Command: %s" % EscapeCommand(output.command) def Truncate(self, str, length): if length and (len(str) > (length - 3)): -- 2.34.1