aboutsummaryrefslogtreecommitdiff
path: root/SrcShared/EmMinimize.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'SrcShared/EmMinimize.cpp')
-rw-r--r--SrcShared/EmMinimize.cpp1310
1 files changed, 1310 insertions, 0 deletions
diff --git a/SrcShared/EmMinimize.cpp b/SrcShared/EmMinimize.cpp
new file mode 100644
index 0000000..16ce4e6
--- /dev/null
+++ b/SrcShared/EmMinimize.cpp
@@ -0,0 +1,1310 @@
+/* -*- mode: C++; tab-width: 4 -*- */
+/* ===================================================================== *\
+ Copyright (c) 2001 Palm, Inc. or its subsidiaries.
+ All rights reserved.
+
+ This file is part of the Palm OS Emulator.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+\* ===================================================================== */
+
+#include "EmCommon.h"
+#include "EmMinimize.h"
+
+#include "EmApplication.h" // gApplication->ScheduleQuit
+#include "EmDlg.h" // MinimizeProgressOpen, DoCommonDialog
+#include "EmEventPlayback.h" // EnableEvents, DisableEvents
+#include "EmPalmOS.h" // EmPalmOS::GenerateStackCrawl
+#include "EmSession.h" // gSession
+#include "Logging.h" // LogAppendMsg
+#include "ROMStubs.h" // DmDatabaseInfo
+#include "Startup.h" // Startup::MinimizeQuitWhenDone
+#include "EmEventOutput.h" // StartGatheringInfo, GatherInfo, OutputEvents
+
+#include "ChunkFile.h" // ChunkFile
+#include "EmStreamFile.h" // EmStreamFile
+#include "SessionFile.h" // SessionFile
+
+#include <math.h> // pow
+#include <strstream> // strstream
+
+/*
+ Minimization Overview
+
+ A Gremlin run may terminate in error after any number of events.
+ However, experience has shown that not all events created by the
+ Gremlin are necessary to reproduce the error. Through minimization,
+ the Palm OS Emulator attempts to find the minimal set of events that
+ will cause an error. The use of "an" rather than "the" is
+ intentional, as we can have no assurance that any error that occurs
+ when intermediate events are cut out is the same error that
+ initially occurred. The distinction is likely academic; what matters
+ is that, given a file describing events that were generated by a
+ Gremlin during a run, minimization will separate the wheat from the
+ chaff, so to speak, by cutting out those events that do not
+ contribute to the error.
+
+ Theory
+
+ Each event generated by a Gremlin can be viewed as a vertex in a
+ directed graph. Each of these events relies on a particular context
+ for its action to be relevant. For example, the first event relies
+ on the root state to be loaded, and nothing else. The vertex or
+ vertices that are responsible for bringing about a context (such as
+ bringing up the Find dialog) have directed edges to all vertices
+ representing actions within this context. If a particular vertex
+ relies on no prior vertices for its context (the first vertex being
+ a prime example), then the only vertex with an edge to it is the
+ vertex representing the root state, which is noteworthy because it
+ is the only vertex with no incoming edges.
+
+ Viewed in this way, it becomes apparent that the problem of
+ minimization is nothing more than finding the minimal set of
+ vertices such that there is an unbroken string of directed edges
+ between said vertices, and that the vertices representing the root
+ state and the error-causing event are both within this set.
+
+ In an alternate interpretation, events are once again vertices, and
+ there are causal directed edges between events; the difference lies
+ in that an edge is present iff the events represented by its
+ endpoints are necessary to cause the error. Then the problem of
+ minmization becomes a vertex cover problem: for reference, see:
+ <http://www.cs.sunysb.edu/~algorith/files/vertex-cover.shtml>.
+
+ Algorithm choice
+
+ Vertex cover is an NP-complete problem, so it is not surprising that
+ we cannot find a polynomial-time (in the number of events) algorithm
+ to find the minimal set of events that will cause an error. Instead,
+ I settle for an algorithm of order O(n^2 log n) -- in the worst
+ case, it may have to make n log n passes, each O(n) because there
+ are n events. This algorithm, while superficially resembling a
+ divide-and-conquer algorithm, really is just a blind uninformed
+ search algorithm that happens to narrow its scope by a factor of 2
+ each time that it cuts out too many events. (Incidentally, I
+ implemented a true divide-and-conquer algorithm as well, of nominal
+ complexity O(n log n), which turned out to be slower on average than
+ this algorithm, because of the sparse nature of this problem space
+ since relatively few events typically contribute to an error.)
+
+ Initially, the algorithm sub-divides the original event ranges into
+ two or more smaller ranges, each range smaller than some hard- coded
+ value (1024 in this implementation). Each range is then considered
+ one at a time by. The range is cast out, and then the remaining
+ events are replayed to see if the crash still occurs. If not, then
+ the "cast out" range is considered non-essential and is permanently
+ removed. If the error no longer occurs, then some event is that
+ range is considered crucial to producing the crash. The range is the
+ sub-divided in an effort to narrow in on which event or events are
+ required. Sub-division continues to the single event level.
+
+ After each of the initial sub-disivisions has been examined in this
+ fashion, Poser checks to see if any events have been permanently
+ removed. If so, the remaining set of events is saved to their own
+ .pev file, and the process begins again with this reduced set of
+ events. Minimization proceeds with this iteration until it has a
+ set of events it can no longer reduce.
+
+ Implementation
+
+ Since the structure of Palm OS Emulator does not allow for a
+ recursive function that controls the entire process of minimization
+ (as would ideally be the case), more novel means are used instead.
+ As noted in the example below, the minimization process works by
+ taking an event range, splitting it in two, and then testing each
+ side to see if it is necessary for an error to occur. If so, then
+ we permanently disable that range of events and try the other half.
+ But if the error did not occur, then we need to split the sub-range
+ in two again and try again.
+
+ The state for the process of splitting event ranges is stored in the
+ fgState member. This variable contains an fLevels member, which is
+ a stack of event ranges. The oldest item on the stack contains the
+ entire event range. The next items on the stack contain the left
+ and right ranges after bisecting the initial range. Each subsequent
+ element on the stack is a further splitting of a range of the
+ previous element.
+
+ Example
+
+ Let us imagine that we have a 8-event Gremlin run that terminates in
+ an error that we wish to minimize. Of these 8 events, numbered
+ 0-indexed from 0-7, only events 0 and 7 are necessary. What these
+ events actually are is inconsequential.
+
+ This is the sequence of events that would transpire:
+
+ LoadEvents would load the .pev file, initializing the event history
+ vector. In Start, an initial event range, whose span is the whole
+ range of events (0-7) would be created. Since I make the (relatively
+ safe) assumption that one cannot eliminate all of the events in a
+ Gremlin run (save for the penultimate and ultimate events) and still
+ hope to encounter the error, the root range's events are not turned
+ off, and instead it spawns two sub-ranges. These two child ranges
+ cover the ranges (0-3) and (4-7), respectively. First the (0-3)
+ range would be selected (this in Start -- for all other selections,
+ NextSubRange is the locale), and events 0-3 turned off by setting
+ appropriate bits in a mask to false. The mask at this point would
+ look like this: f-f-f-f-t-t-t-t. Since event 0 is necessary to
+ reproduce the error, we would reach NoErrorOccurred. This means
+ that we now knew that something in the range (0-3) was important,
+ and that we needed to further examine that range. Thus, we end up
+ calling SplitAndStartAgain.
+
+ In NoErrorOccurred, (0-3) would have its events turned on, and then
+ SplitAndStartAgain would sub-divide the range into (0-1) and (2-3).
+ The event mask of (0-1) would then be masked (ie, the events would
+ not be replayed). At this point the event mask would look like this:
+ f-f-t-t-t-t-t-t. Again, we would not find the error, since event 0
+ would still not be replayed. The process would then be repeated,
+ with the ramge (0-1) spawning two sub-ranges of (0-0) and (1-1). The
+ left child of (0-1), (0-0) would be selected to run next. The event
+ mask would look like this: f-t-t-t-t-t-t-t.
+
+ This time, since we would be playing back all events except for 0,
+ the run would again not terminate in error. At this point we will
+ have hit our depth limit, so in NoErrorOccurred, the events in the
+ current range (only event 0) would be turned on. Then, in
+ SplitAndStartAgain, we would realize that we could no longer split
+ the event range. We would then call NextSubRange, which would
+ select (1-1) from the stack of event ranges still left to examine.
+ The event mask at this point would look like this: t-f-t-t-t-t-t-t.
+ Its run would terminate in error, so from
+ ErrorHandling::HandleDialog, ErrorOccurred would be called, not
+ NoErrorOccurred. This means that the range (1-1) will not spawn
+ children of its own, or have the events within its range turned back
+ on. Instead, ErrorOccurred will climb up the stack to (2-3). Its
+ events would be turned off, making the event mask look like this:
+ t-f-f-f-t-t-t-t.
+
+ Since events 0 and 7 are both turned on, this run would terminate in
+ error. In ErrorOccurred, event range (4-7) would be the next set of
+ events to examine. When (4-7)'s events are turned off, event 7 would
+ be turned off, and the error would no longer occur. The process
+ would then continue until events 0 and 7 were the only events
+ remaining, where the event mask is t-f-f-f-f-f-f-t. This run would
+ terminate in error, since events 0 and 7 would be played back.
+
+ In ErrorOccurred, we'd see that the event range stack would be
+ exhausted. We'd then save out the remaining events (0 and 7) to a
+ .pev file and run the whole process over again to see if any further
+ progress could be made. In our hypothetical situation, we'd find
+ that events 0 and 7 would still be needed.
+
+ Next, we would make one last pass over the remaining events. This
+ time, it's not to see if any events can be removed. Instead, it's
+ to collect contextual information useful in creating an English
+ report for the user. In particular, we'd find out what, if any
+ objects on the screen were hit by pen events so that we could report
+ then instead of telling the user to tap at, say, screen location 53,
+ 112.
+
+ Finally, the ultimate minimal set of events is written to a .pev
+ file suitable for replaying, and are are converted into a set of
+ English instructions that, when followed by a human, should result
+ in the same crash as replaying the events file does.
+
+ Improvements
+
+ The current algorithm, while useful, could still be improved in many
+ ways. Following are some ideas that occur to us, in case some
+ developer feels the need to implement them for us. :-)
+
+ * Semantic subdivision
+
+ Currently, the algorithm makes little distinction as to where it
+ decides to sub-divide event ranges. Other than the initial set of
+ ranges (where Poser keeps dividing until each sub-range is fewer
+ than some fairly arbitrary upper limit) and the single-event ranges
+ (where Poser gives pen-up events special treatment), Poser merely
+ takes an event range and divides it in two.
+
+ Instead, it would be good to first scope out the set of events and
+ mark out good division points. At the first level, Poser would find
+ all places where application switches take place and set division
+ points at those events. The theory here is that if the Address Book
+ crashes, what previously occurred in the Calculator application is
+ of little consequence. A pass over the events would then be made,
+ attempting to cast out events that occur between app switch events.
+
+ Next, the algorithm would attempt the same thing with forms. Again,
+ the theory is that if you crash in Form B, what just occurred in
+ Form A has a good chance of being irrelevent. Of course, this is
+ not a universally true statement, but that's why we perform our
+ tests and see what happens. For the cases where Forms A and B are
+ not strongly connected, we can cut out a whole swath of events at
+ one blow.
+
+ Next, we examine events at a key and pen sequence level. For key
+ sequences, we assume that there's little difference between one key
+ event and many key events. Therefore, we trim down any key
+ sequences to just the first key and try a run with just that event.
+ If the crash still occurs, we've just made a big win. Similarly,
+ with pen sequences, if there are three or more pen-down events
+ before the final pen-up event, we can try to remove all events
+ between the first and last pen down events. The idea here is that
+ only the initial pen down location and the final pen up location are
+ likely to be important, and that cutting out everything in between
+ would probably be OK.
+
+ Finally, we can make a pass using the current algorithm that slices
+ and dices down to the single event level. At this point, we
+ probably won't be removing too many events, but should also have
+ relatively few, so the slice and dice process shouldn't take very
+ long.
+
+ * Menu descriptions
+
+ Currently, taps on menu items are reported as "Tap at x,y". Where
+ possibly, this is annotated with the name of the dialog that is
+ brought up because of that menu selection. But it would ultimately
+ be better to provide the text of the menu and menu item.
+
+ * HTML output
+
+ Currently, the English translation is provided in a plain text file.
+ What might be nicer is an HTML file that can reference screen shots
+ showing what's going on at particular points. For instance,
+ sometimes saying "Tap on control "Foo"" is not enough -- sometimes
+ it's important to know where on "Foo" was tapped if it were a
+ multipart control. Being able to see a screen shot with cross-hairs
+ at the point where the user is supposed to tap would be a big help.
+
+ * Tap-drag
+
+ A sequence of pen-down events followed by a pen-up event are
+ currently reported as a series of "Tap at x,y" messages. Instead,
+ they should be reported as "Tap at x,y", "Drag to x,y", etc.
+ messages.
+*/
+
+
+#if _DEBUG
+#define LOG_MINIMIZATION 1
+#else
+#define LOG_MINIMIZATION 0
+#endif
+
+#define PRINTF if (!LOG_MINIMIZATION) ; else LogAppendMsg
+
+static const int kMaxRange = 1024; // Don't handle any ranges larger than this.
+static const int kLastPass = 0; // Indicates we're making a last pass
+
+omni_mutex EmMinimize::fgMutex;
+EmMinimize::EmMinimizeState EmMinimize::fgState;
+Bool EmMinimize::fgIsOn;
+uint32 EmMinimize::fgStartTime;
+long EmMinimize::fgInitialNumberOfEvents;
+long EmMinimize::fgDiscardedNumberOfEvents;
+long EmMinimize::fgPassNumber;
+Bool EmMinimize::fgPassEndedInError;
+StringList EmMinimize::fgLastStackCrawl;
+
+
+static inline Bool PrvIsPenUp (const PointType& pt)
+{
+ return (pt.x == -1) && (pt.y == -1);
+}
+
+static inline Bool PrvIsPenDown (const PointType& pt)
+{
+ return !::PrvIsPenUp (pt);
+}
+
+
+#pragma mark -
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::Initialize
+// ---------------------------------------------------------------------------
+
+void EmMinimize::Initialize (void)
+{
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::Reset
+// ---------------------------------------------------------------------------
+
+void EmMinimize::Reset (void)
+{
+ EmMinimize::TurnOn (false);
+ fgState.fLevels.clear ();
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::Save
+// ---------------------------------------------------------------------------
+
+void EmMinimize::Save (SessionFile&)
+{
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::Load
+// ---------------------------------------------------------------------------
+
+void EmMinimize::Load (SessionFile&)
+{
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::Dispose
+// ---------------------------------------------------------------------------
+
+void EmMinimize::Dispose (void)
+{
+ EmMinimize::TurnOn (false);
+ fgState.fLevels.clear ();
+}
+
+
+#pragma mark -
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::Start
+// ---------------------------------------------------------------------------
+// Startup the entire minimization process. Called after a document is loaded
+// with the intent of minimizaing the events stored with it.
+
+void EmMinimize::Start (void)
+{
+ // Load the events. Events are not loaded by default when the rest of
+ // the session loads. See comments for EmEventPlayback::Load ().
+
+ EmMinimize::LoadEvents ();
+
+ // Enable all events
+
+ EmEventPlayback::EnableEvents ();
+
+ // Check that this is a stream that ended in error.
+
+ fgInitialNumberOfEvents = EmMinimize::FindFirstError ();
+ if (fgInitialNumberOfEvents < 0)
+ {
+ EmDlg::DoCommonDialog ("Could not find an error.", kDlgFlags_OK);
+ return;
+ }
+
+ // Get rid of the error event and all events after it. The error event
+ // will be returned at the end of the minimization process, but for now,
+ // it just get's in the way.
+
+ EmEventPlayback::DisableEvents (fgInitialNumberOfEvents);
+ EmEventPlayback::CullEvents ();
+
+ // Re-update fgInitialNumberOfEvents; the number of events could have been
+ // decreased further than expected if there were consecutive pen-up events
+ // in the stream (Gremlins *can* generate that).
+
+ fgInitialNumberOfEvents = EmEventPlayback::CountNumEvents ();
+
+ // Initialize the minimization routines.
+
+ fgState.fLevels.clear ();
+ fgStartTime = Platform::GetMilliseconds ();
+ fgDiscardedNumberOfEvents = 0;
+ fgPassNumber = 1;
+
+ EmMinimize::TurnOn (true);
+ EmMinimize::InitialLevel ();
+ EmMinimize::SplitAndStartAgain ();
+
+ // Open the progress dialog.
+
+ EmDlg::MinimizeProgressOpen ();
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::Stop
+// ---------------------------------------------------------------------------
+// Stop the entire minimization process. Called when the user presses the
+// "Stop" button in the Minimization Control dialog.
+
+void EmMinimize::Stop (void)
+{
+ EmMinimize::TurnOn (false);
+ EmEventPlayback::ReplayEvents (false);
+ EmEventOutput::GatherInfo (false);
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::TurnOn
+// ---------------------------------------------------------------------------
+
+void EmMinimize::TurnOn (Bool newState)
+{
+ fgIsOn = newState;
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::IsOn
+// ---------------------------------------------------------------------------
+
+Bool EmMinimize::IsOn (void)
+{
+ return fgIsOn /* && EmEventPlayBack::ReplayingEvents () ? */;
+}
+
+
+#pragma mark -
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::NoErrorOccurred
+// ---------------------------------------------------------------------------
+// The current set of events were replayed with no error occurring.
+// Therefore, we need to turn the most recent set of events back on, split
+// it, and try again.
+
+void EmMinimize::NoErrorOccurred (void)
+{
+ PRINTF ("EmMinimize::NoErrorOccurred:");
+
+ fgPassEndedInError = false;
+
+ // Turn current set of events back on.
+
+ long begin, end;
+ EmMinimize::CurrentRange (begin, end);
+ PRINTF ("EmMinimize::NoErrorOccurred: re-enabling events %ld - %ld.", begin, end - 1);
+
+ EmEventPlayback::EnableEvents (begin, end);
+
+ // If we were doing the "last pass" and got here anyway, we're pretty
+ // messed up. The "last pass" was supposed to be guaranteed to end in an
+ // error, but it didn't. Oh well. Finish up and tell the user what
+ // happened as best as we can.
+
+ if (EmMinimize::fgPassNumber == kLastPass)
+ {
+ fgState.fLevels.clear ();
+ EmMinimize::MinimizationPassComplete ();
+ return;
+ }
+
+ // Set the bit that says we examined this range and found it interesting.
+
+ fgState.fLevels.back ().fChecked = true;
+
+ // Split the current range and try again.
+
+ EmMinimize::SplitAndStartAgain ();
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::ErrorOccurred
+// ---------------------------------------------------------------------------
+// The current set of events resulted in an error occurring. Therefore, we
+// can keep the current set of events turned off, and try to find others to
+// turn off.
+
+void EmMinimize::ErrorOccurred (void)
+{
+ PRINTF ("EmMinimize::ErrorOccurred:");
+
+ fgPassEndedInError = true;
+
+ // Capture the current stack crawl in case we need it when reporting
+ // failure on the last pass to produce an error.
+
+ EmMinimize::GenerateStackCrawl (fgLastStackCrawl);
+
+ // Keep the current set of events turned off.
+
+ long begin, end;
+ EmMinimize::CurrentRange (begin, end);
+ PRINTF ("EmMinimize::ErrorOccurred: discarding events %ld - %ld.", begin, end - 1);
+
+ // Update the number of events we've discarded. Note that we update this
+ // value by calling CountEnabledEvents instead of adjusting by (end -
+ // begin). That's because some events are not discarded even though we've
+ // asked them to because they are too important to discard (e.g. pen up
+ // events).
+
+ fgDiscardedNumberOfEvents = fgInitialNumberOfEvents - EmEventPlayback::CountEnabledEvents ();
+
+ // If the error occurred before the last event was played, then let's
+ // disable all subsequent events and start afresh.
+
+ long currentEvent = EmEventPlayback::GetCurrentEvent ();
+ long numEvents = EmEventPlayback::GetNumEvents ();
+
+ if (currentEvent < numEvents)
+ {
+ PRINTF ("EmMinimize::ErrorOccurred: error occurred at event %ld of %ld.",
+ currentEvent, numEvents);
+
+ omni_mutex_lock lock (fgMutex);
+
+ // Disable all events past the one that just caused the error.
+
+ EmEventPlayback::DisableEvents (currentEvent);
+
+ // Clear the event range stack so that we can start a new pass.
+
+ fgState.fLevels.clear ();
+
+ // Start a new pass. This will also cull the events we just disabled.
+
+ EmMinimize::MinimizationPassComplete ();
+
+ return;
+ }
+
+ // Pop the current range and try the next one.
+
+ EmMinimize::NextSubRange ();
+}
+
+
+#pragma mark -
+
+uint32 EmMinimize::GetPassNumber (void)
+{
+ return fgPassNumber;
+}
+
+
+uint32 EmMinimize::GetElapsedTime (void)
+{
+ return Platform::GetMilliseconds () - fgStartTime;
+}
+
+
+void EmMinimize::GetCurrentRange (uint32& ubegin, uint32& uend)
+{
+ long begin, end;
+ EmMinimize::CurrentRange (begin, end);
+
+ ubegin = begin;
+ uend = end;
+}
+
+
+uint32 EmMinimize::GetNumDiscardedEvents (void)
+{
+ return fgDiscardedNumberOfEvents;
+}
+
+
+uint32 EmMinimize::GetNumInitialEvents (void)
+{
+ return fgInitialNumberOfEvents;
+}
+
+
+#pragma mark -
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::MinimizationPassComplete
+// ---------------------------------------------------------------------------
+// We've finished splitting and replaying events. Examine the results to see
+// if we're done, or if we need to make another Minimization passing using the
+// remaining events.
+
+void EmMinimize::MinimizationPassComplete (void)
+{
+ // Get the initial number of events, remove the masked-out events, and
+ // then get the remaining number of events.
+
+ long oldNumEvents = EmEventPlayback::GetNumEvents ();
+
+ EmEventPlayback::CullEvents ();
+
+ long newNumEvents = EmEventPlayback::GetNumEvents ();
+
+ // If significant culling occurred, then let's make another pass.
+
+ if (EmMinimize::MakeAnotherPass (oldNumEvents, newNumEvents))
+ {
+ PRINTF ("EmMinimize::MinimizationPassComplete: making another pass.");
+ PRINTF (" oldNumEvents = %ld", oldNumEvents);
+ PRINTF (" newNumEvents = %ld", newNumEvents);
+
+ ++fgPassNumber;
+
+ EmMinimize::InitialLevel ();
+ EmMinimize::SplitAndStartAgain ();
+ }
+
+ // Make a final pass in order to collect context information
+
+ else if (fgPassNumber != kLastPass)
+ {
+ PRINTF ("EmMinimize::MinimizationPassComplete: making last pass.");
+ PRINTF (" oldNumEvents = %ld", oldNumEvents);
+ PRINTF (" newNumEvents = %ld", newNumEvents);
+
+ fgPassNumber = kLastPass;
+
+ // Tell EmEventOutput to wake up and start paying attention.
+
+ EmEventOutput::StartGatheringInfo ();
+
+ EmMinimize::InitialLevel ();
+ EmMinimize::StartAgain ();
+ }
+
+ // Otherwise, we are done!
+
+ else
+ {
+ PRINTF ("EmMinimize::MinimizationPassComplete: DONE.");
+ PRINTF (" oldNumEvents = %ld", oldNumEvents);
+ PRINTF (" newNumEvents = %ld", newNumEvents);
+
+ EmMinimize::MinimizationComplete ();
+ }
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::MinimizationComplete
+// ---------------------------------------------------------------------------
+// Minimization is completely done. Save the results, write out a text
+// version of the events, and tell the user.
+
+void EmMinimize::MinimizationComplete (void)
+{
+ PRINTF ("EmMinimize::MinimizationComplete: DONE.");
+
+ // Stop the presses.
+
+ EmMinimize::TurnOn (false);
+ EmEventPlayback::ReplayEvents (false);
+ EmEventOutput::GatherInfo (false);
+
+ // Save the minimal set of events.
+
+ EmMinimize::SaveMinimalEvents ();
+
+ // Convert the events to an English description.
+
+ EmMinimize::OutputEventsAsEnglish ();
+
+ // Reset to our initial state (as opposed to the error state we're
+ // currently in).
+
+ EmMinimize::LoadInitialState ();
+
+ if (Startup::MinimizeQuitWhenDone ())
+ {
+ gApplication->ScheduleQuit ();
+ }
+ else
+ {
+ // Show a dialog telling the user about the results.
+
+ char buffer[200];
+ sprintf (buffer, "Minimization has been completed. %ld of %ld events discarded.",
+ fgDiscardedNumberOfEvents, fgInitialNumberOfEvents);
+
+ EmDlg::DoCommonDialog (buffer, kDlgFlags_OK);
+ }
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::SaveMinimalEvents
+// ---------------------------------------------------------------------------
+
+void EmMinimize::SaveMinimalEvents (void)
+{
+ // Get the original event file. We'll save the events in the same
+ // directory but with a modified name.
+
+ EmFileRef oldEventRef = gSession->GetFile ();
+ string oldEventName = oldEventRef.GetName ();
+
+ // Convert the name from <Foo>.pev to <Foo>_Min.pev.
+
+ string newEventName = oldEventName;
+
+ if (::EndsWith (newEventName.c_str (), ".pev"))
+ {
+ newEventName = newEventName.substr (0, newEventName.size () - 4);
+ }
+
+ newEventName += "_Min.pev";
+
+ // Copy the old session file to the new one that will hold the minimized
+ // event set.
+
+ EmFileRef newEventRef (oldEventRef.GetParent (), newEventName);
+
+ EmStreamFile oldEventStream (oldEventRef, kOpenExistingForRead,
+ kFileCreatorEmulator, kFileTypeEvents);
+ EmStreamFile newEventStream (newEventRef, kCreateOrEraseForWrite,
+ kFileCreatorEmulator, kFileTypeEvents);
+
+ ChunkFile oldEventChunkFile (oldEventStream);
+ ChunkFile newEventChunkFile (newEventStream);
+
+ int index = 0;
+ ChunkFile::Tag tag;
+ Chunk chunk;
+
+ while (oldEventChunkFile.ReadChunk (index, tag, chunk))
+ {
+ // Copy all chunks except the previous (pre-minimized) event set.
+ // We'll be adding the minimized event set to the file later.
+
+ if (tag != /*SessionFile::kGremlinHistory*/ 'hist')
+ {
+ newEventChunkFile.WriteChunk (tag, chunk);
+ }
+
+ ++index;
+ }
+
+ // When starting minimization, we removed all events beyond and including
+ // the terminating error event. Before saving out the new event set, add
+ // back the error event.
+
+ EmEventPlayback::RecordEvents (true);
+ EmEventPlayback::RecordErrorEvent ();
+ EmEventPlayback::RecordEvents (false);
+
+ // Now write the final event set.
+
+ SessionFile newEventSessionFile (newEventChunkFile);
+ EmEventPlayback::SaveEvents (newEventSessionFile);
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::OutputEventsAsEnglish
+// ---------------------------------------------------------------------------
+
+void EmMinimize::OutputEventsAsEnglish (void)
+{
+ EmEventPlayback::LogEvents ();
+
+ // Create a string stream to which we will write out results. After we
+ // completely buffer the output, we'll write it to a file. We don't write
+ // directly to the file so that we can take advantage of the
+ // integer-to-text conversion facilities of ostream.
+
+ strstream stream;
+
+ // Write out our initial message.
+
+ EmFileRef ref = gSession->GetFile ();
+ string path = ref.GetFullPath ();
+
+ stream << "=== Minimal Events: (" << path << ")" << endl;
+
+ // Write out events.
+
+ EmEventOutput::OutputEvents (stream);
+
+ // If the last pass managed to fail to produce an error, tell the user
+ // about it, and show them the competing stack crawls.
+
+ if (!EmMinimize::fgPassEndedInError)
+ {
+ StringList currentStackCrawl;
+ EmMinimize::GenerateStackCrawl (currentStackCrawl);
+
+ stream << endl;
+ stream << "WARNING WARNING WARNING" << endl;
+ stream << endl;
+ stream << "After the minimization process produces a minimal set of events" << endl;
+ stream << "leading to a crash, it makes one last run through the events," << endl;
+ stream << "collecting context information used to create the descriptions" << endl;
+ stream << "above. However, on this last pass, no error occurred. Following" << endl;
+ stream << "are the current stack crawl and the stack crawl captured the last" << endl;
+ stream << "time a crash occurred." << endl;
+ stream << endl;
+
+ stream << "Current call stack:" << endl;
+ EmEventOutput::OutputStack (stream, currentStackCrawl);
+ stream << endl;
+
+ stream << "Last error-producing call stack:" << endl;
+ EmEventOutput::OutputStack (stream, fgLastStackCrawl);
+ stream << endl;
+ }
+
+ // Write out final message.
+
+ stream << "=== Event listing complete." << endl;
+
+ // Get the original event file. We'll save the events in the same
+ // directory but with a modified name.
+
+ // Convert the name from <Foo>.pev to <Foo>_Min.pev.
+
+ string textEventName = ref.GetName ();
+
+ if (::EndsWith (textEventName.c_str (), ".pev"))
+ {
+ textEventName = textEventName.substr (0, textEventName.size () - 4);
+ }
+
+ textEventName += "_Min.txt";
+
+ // Create the output file.
+
+ EmFileRef textEventRef (ref.GetParent (), textEventName);
+ EmStreamFile textEventStream (textEventRef, kCreateOrEraseForWrite | kOpenText,
+ kFileCreatorCodeWarrior, kFileTypeText);
+
+ // Write the event text to it.
+
+ textEventStream.PutBytes (stream.str (), stream.pcount ());
+
+ // Unfreeze the stream, or else its storage will be leaked.
+
+ stream.freeze (false);
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::MakeAnotherPass
+// ---------------------------------------------------------------------------
+// Return whether or not we feel that a significant amount of event culling
+// had occurred in the pass. If so, we'll want to make another pass.
+
+Bool EmMinimize::MakeAnotherPass (long oldNumEvents, long newNumEvents)
+{
+ // Make another pass if we've reduced the number of events in the previous
+ // pass by more than 10%.
+
+ return newNumEvents < oldNumEvents;
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::CurrentRange
+// ---------------------------------------------------------------------------
+// Return the current event range that we are examining.
+
+void EmMinimize::CurrentRange (long& begin, long& end)
+{
+ omni_mutex_lock lock (fgMutex);
+
+ if (fgState.fLevels.size () > 0)
+ {
+ begin = fgState.fLevels.back ().fBegin;
+ end = fgState.fLevels.back ().fEnd;
+ }
+ else
+ {
+ begin = 0;
+ end = 0;
+ }
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::InitialLevel
+// ---------------------------------------------------------------------------
+// Set up our "fLevels" member with the initial left/right event sub-ranges.
+
+void EmMinimize::InitialLevel (void)
+{
+ EmAssert (fgState.fLevels.size () == 0);
+
+ // Establish the initial event range.
+
+ EmMinimizeLevel newLevel;
+
+ newLevel.fBegin = 0;
+ newLevel.fEnd = EmEventPlayback::GetNumEvents ();
+ newLevel.fChecked = false;
+
+ omni_mutex_lock lock (fgMutex);
+
+ fgState.fLevels.push_back (newLevel);
+
+#if LOG_MINIMIZATION
+ {
+ PRINTF ("EmMinimize::InitialLevel: Added first level.");
+
+ EmMinimizeLevelList::iterator iter = fgState.fLevels.begin ();
+
+ while (iter != fgState.fLevels.end ())
+ {
+ PRINTF (" %ld:", iter - fgState.fLevels.begin ());
+ PRINTF (" fBegin: %ld", iter->fBegin);
+ PRINTF (" fEnd: %ld", iter->fEnd);
+ PRINTF (" fChecked: %s", iter->fChecked ? "TRUE" : "FALSE");
+
+ ++iter;
+ }
+ }
+#endif
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::SplitCurrentLevel
+// ---------------------------------------------------------------------------
+// Create a new level based on the given range. The given range is split in
+// two, records are created holding those two sub-ranges, and the records are
+// pushed onto our stack. Returns false if it was unable to create the two
+// new ranges (either because the current range is too narrow or too deep).
+
+Bool EmMinimize::SplitCurrentLevel (void)
+{
+ omni_mutex_lock lock (fgMutex);
+
+ EmAssert (fgState.fLevels.size () > 0);
+
+ // Split the next event range until it's of a manageable size.
+
+ while (1)
+ {
+ EmMinimizeLevel prevLevel = fgState.fLevels.back ();
+ long prevRange = prevLevel.fEnd - prevLevel.fBegin;
+
+ // If we're at the smallest range, leave and say we can't split this
+ // atom.
+
+ if (prevRange <= 1)
+ return false;
+
+ // Pop the top event range, split it, and put it back.
+
+ long midpoint = (prevLevel.fBegin + prevLevel.fEnd) / 2;
+
+ EmAssert (prevLevel.fBegin < midpoint);
+ EmAssert (midpoint < prevLevel.fEnd);
+
+ fgState.fLevels.pop_back ();
+
+ EmMinimizeLevel newLevel;
+
+ newLevel.fBegin = midpoint;
+ newLevel.fEnd = prevLevel.fEnd;
+ newLevel.fChecked = prevLevel.fChecked && (newLevel.fEnd - newLevel.fBegin) > 1;
+
+ fgState.fLevels.push_back (newLevel);
+
+ newLevel.fBegin = prevLevel.fBegin;
+ newLevel.fEnd = midpoint;
+ newLevel.fChecked = prevLevel.fChecked && (newLevel.fEnd - newLevel.fBegin) > 1;
+
+ fgState.fLevels.push_back (newLevel);
+
+ // If the range is now small enough, leave and say we have new ranges
+ // to deal with.
+
+ if (newLevel.fEnd - newLevel.fBegin <= kMaxRange)
+ break;
+ }
+
+#if LOG_MINIMIZATION
+ {
+ EmMinimizeLevelList::iterator iter = fgState.fLevels.begin ();
+
+ while (iter != fgState.fLevels.end ())
+ {
+ PRINTF (" %ld:", iter - fgState.fLevels.begin ());
+ PRINTF (" fBegin: %ld", iter->fBegin);
+ PRINTF (" fEnd: %ld", iter->fEnd);
+ PRINTF (" fChecked: %s", iter->fChecked ? "TRUE" : "FALSE");
+
+ ++iter;
+ }
+ }
+#endif
+
+ return true;
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::StartAgain
+// ---------------------------------------------------------------------------
+// Queue things up to start again. Start the playback mechanism, and reload
+// the initial state.
+
+void EmMinimize::StartAgain (void)
+{
+ PRINTF ("EmMinimize::StartAgain: starting over.");
+
+ EmEventPlayback::ReplayEvents (true);
+ EmMinimize::LoadInitialState ();
+
+ LogDump ();
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::SplitAndStartAgain
+// ---------------------------------------------------------------------------
+// Queue things up to start again. Start the playback mechanism, and reload
+// the initial state.
+
+void EmMinimize::SplitAndStartAgain (void)
+{
+ // Try to split the range. We may not be able to do that if the range is
+ // too narrow or we've split too many times already.
+
+ if (EmMinimize::SplitCurrentLevel ())
+ {
+ PRINTF ("EmMinimize::SplitAndStartAgain: split current range.");
+
+ // Attempt to disable the current range and start again. This may
+ // fail if we try to do something like disable a solitary pen-up
+ // event, in which case we'll move to the NextSubRange.
+
+ EmMinimize::DisableAndStartAgain ();
+ }
+ else
+ {
+ // We weren't able to split the current event range. That must mean
+ // that the event(s) in this range were important, and that we may
+ // have found the reason that the initial range we started subdivided
+ // is interesting. Assuming that, clear the fChecked bit on all
+ // entries on our stack, forcing us to retest them before splitting
+ // them.
+
+ {
+ omni_mutex_lock lock (fgMutex);
+
+ EmMinimizeLevelList::iterator iter = fgState.fLevels.begin ();
+ while (iter != fgState.fLevels.end ())
+ {
+ iter->fChecked = false;
+ ++iter;
+ }
+ }
+
+ // Pop the current range off and work with the next one.
+
+#if LOG_MINIMIZATION
+ EmMinimizeLevel level = fgState.fLevels.back ();
+
+ PRINTF ("EmMinimize::SplitAndStartAgain: too deep or narrow -- moving along.");
+ PRINTF (" begin: %ld", level.fBegin);
+ PRINTF (" end: %ld", level.fEnd);
+ PRINTF (" checked: %s", level.fChecked ? "TRUE" : "FALSE");
+#endif
+
+ EmMinimize::NextSubRange ();
+ }
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::DisableAndStartAgain
+// ---------------------------------------------------------------------------
+// Attempt to disable the current range and start again. This may fail if we
+// try to do something like disable a solitary pen-up event, in which case
+// we'll move to the NextSubRange.
+
+void EmMinimize::DisableAndStartAgain (void)
+{
+ long begin, end;
+ EmMinimize::CurrentRange (begin, end);
+
+ PRINTF ("EmMinimize::DisableAndStartAgain: disabling events %ld - %ld.", begin, end - 1);
+
+ // Don't try disabling pen-up events. If the pen-up event is preceded by
+ // one or more pen-down events, then EmEventPlayback won't let it be
+ // disabled. If the pen-up event is not preceded by a pen-down event,
+ // then EmEventPlayback will have already filtered it out. Either way, we
+ // don't get anything by trying to disable such an event and replaying the
+ // event stream all over again.
+
+ if (end - begin == 1)
+ {
+ EmRecordedEvent event;
+ EmEventPlayback::GetEvent (begin, event);
+
+ if (event.eType == kRecordedPenEvent && ::PrvIsPenUp (event.penEvent.coords))
+ {
+ PRINTF ("EmMinimize::DisableAndStartAgain: not disabling pen up event.");
+ NextSubRange ();
+ return;
+ }
+ }
+
+ // Turn off the events in the current sub-range.
+
+ EmEventPlayback::DisableEvents (begin, end);
+
+ // Start again.
+
+ EmMinimize::StartAgain ();
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::NextSubRange
+// ---------------------------------------------------------------------------
+// Pop an event range off of the stack and start working with the previous
+// range that's now on the top of the stack.
+
+void EmMinimize::NextSubRange (void)
+{
+ {
+ omni_mutex_lock lock (fgMutex);
+
+ PRINTF ("EmMinimize::NextSubRange: popping a level.");
+
+ EmAssert (fgState.fLevels.size () > 0);
+
+ fgState.fLevels.pop_back ();
+ }
+
+ // If there are no more ranges, we are done with this pass. Check the
+ // results to see if we'd like to make another pass.
+
+ if (fgState.fLevels.size () == 0)
+ {
+ EmMinimize::MinimizationPassComplete ();
+ return;
+ }
+
+ // Determine what to do with the range that's now at the top of the stack.
+ // Either we start testing it directly, or we see if we can skip that
+ // step and start splitting it. If we guess correctly, we can save a lot
+ // of time. If we assume we don't have to split this range first, then
+ // testing it in toto may show that we can get rid of it all right now.
+ // Otherwise, we'd end up splitting it down to the single event level
+ // before we discovered we didn't need any of the events. On the other
+ // hand, if we're fairly sure that we need to closely examine this range,
+ // we save time by not testing it in toto first, and proceed directly to
+ // splitting it.
+ //
+ // We maintain a Boolean flag that tells us whether or not to test the
+ // entire range before splitting or to split before testing each half. As
+ // well, we split any range that is larger than our hardcoded upper limit
+ // on event ranges.
+
+ EmMinimizeLevel currLevel = fgState.fLevels.back ();
+
+ if (currLevel.fChecked || ((currLevel.fEnd - currLevel.fBegin) > kMaxRange))
+ {
+ EmMinimize::SplitAndStartAgain ();
+ }
+ else
+ {
+ PRINTF ("EmMinimize::NextSubRange: disabling events %ld - %ld.",
+ currLevel.fBegin, currLevel.fEnd - 1);
+
+ // Attempt to disable the current range and start again. This may
+ // fail if we try to do something like disable a solitary pen-up
+ // event, in which case we'll move to the NextSubRange.
+
+ EmMinimize::DisableAndStartAgain ();
+ }
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::LoadInitialState
+// ---------------------------------------------------------------------------
+// Reload the current file so that we can start pelting it with events again.
+
+void EmMinimize::LoadInitialState (void)
+{
+ // Queue up a call to RealLoadInitialState.
+
+ gSession->ScheduleMinimizeLoadState ();
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::RealLoadInitialState
+// ---------------------------------------------------------------------------
+// Reload the current file so that we can start pelting it with events again.
+
+void EmMinimize::RealLoadInitialState (void)
+{
+ ErrCode result = errNone;
+
+ try
+ {
+ EmAssert (gSession);
+ gSession->Load (gSession->GetFile ());
+
+ PRINTF ("EmMinimize::RealLoadInitialState: Reloaded initial state.");
+ }
+ catch (ErrCode errCode)
+ {
+ result = errCode;
+ }
+
+ // !!! Should probably do something on failure!
+
+// return result;
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::LoadEvents
+// ---------------------------------------------------------------------------
+
+void EmMinimize::LoadEvents (void)
+{
+ EmAssert (gSession);
+
+ EmEventPlayback::LoadEvents (gSession->GetFile ());
+
+ PRINTF ("EmMinimize::LoadEvents: loaded %d events.", EmEventPlayback::GetNumEvents ());
+}
+
+
+// ---------------------------------------------------------------------------
+// ¥ EmMinimize::FindFirstError
+// ---------------------------------------------------------------------------
+// Return the index of the first error event record. Return -1 if one could
+// not be found.
+
+long EmMinimize::FindFirstError (void)
+{
+ // The actual implementation for this function is over in EmEventPlayback
+ // for now. The current EmEventPlayback interface and implementation
+ // doesn't support the efficient retrieval of arbitrary events, so we put
+ // the whole FindFirstError implementation over there.
+
+ return EmEventPlayback::FindFirstError ();
+
+/*
+ EmRecordedEvent event;
+ long numEvents = EmEventPlayback::GetNumEvents ();
+
+ for (long ii = 0; ii < numEvents; ++ii)
+ {
+ EmEventPlayback::GetEvent (ii, event);
+
+ if (event.eType == kRecordedErrorEvent)
+ {
+ PRINTF ("EmMinimize::FindFirstError: found an error event.");
+ return ii;
+ }
+ }
+
+ PRINTF ("EmMinimize::FindFirstError: failed to find an error event.");
+ return -1;
+*/
+}
+
+void EmMinimize::GenerateStackCrawl (StringList& stackCrawl)
+{
+ // Capture the stack crawl.
+
+ EmStackFrameList stackFrames;
+ EmPalmOS::GenerateStackCrawl (stackFrames);
+
+ // Generate a full stack crawl.
+
+ ::StackCrawlStrings (stackFrames, stackCrawl);
+}