-- Copyright (c) 2004 Duncan Coutts -- This program is liscenced under the GNU General Public License version 2 -- or (at your option) any later version. -- This is a slightly larger demo that combines use of glade, the file chooser -- dialog, program state (IORefs) and use of the mogul tree view wrapper -- interface. -- The program is a simple viewer for the log files that ghc produces when you -- do time profiling. The parser is not very clever so loading large files can -- take several seconds. -- TODO: The gui will appear to hang when loading files. We should use threads -- to keep the gui responsive. module Main where import Graphics.UI.Gtk import Graphics.UI.Gtk.Glade import Graphics.UI.Gtk.ModelView as New import ParseProfile import Data.Maybe (isJust, fromJust) import Control.Monad (when) import Data.List (unfoldr, intersperse) import qualified Data.Tree as Tree import System.Environment (getArgs) import Data.IORef main :: IO () main = do -- our global state thresholdVar <- newIORef 0 --current cuttoff/threshhold value profileVar <- newIORef Nothing --holds the current profile data structure -- initialisation stuff initGUI Just dialogXml <- xmlNew "ProfileViewer.glade" -- get a handle on a various objects from the glade file mainWindow <- xmlGetWidget dialogXml castToWindow "mainWindow" onDestroy mainWindow mainQuit mainView <- xmlGetWidget dialogXml castToTreeView "mainView" titleLabel <- xmlGetWidget dialogXml castToLabel "titleLabel" commandLabel <- xmlGetWidget dialogXml castToLabel "commandLabel" totalTimeLabel <- xmlGetWidget dialogXml castToLabel "totalTimeLabel" totalAllocLabel <- xmlGetWidget dialogXml castToLabel "totalAllocLabel" -- create the tree model store <- New.treeStoreNew [] New.treeViewSetModel mainView store let createTextColumn name field = do column <- New.treeViewColumnNew New.treeViewAppendColumn mainView column New.treeViewColumnSetTitle column name cell <- New.cellRendererTextNew New.treeViewColumnPackStart column cell True New.cellLayoutSetAttributes column cell store (\record -> [New.cellText := field record]) -- create the various columns in both the model and view createTextColumn "Cost Centre" costCentre createTextColumn "Module" moduleName createTextColumn "Entries" (show.entries) createTextColumn "Individual %time" (show.(/10).fromIntegral.individualTime) createTextColumn "Individual %alloc" (show.(/10).fromIntegral.individualAlloc) createTextColumn "Inherited %time" (show.(/10).fromIntegral.inheritedTime) createTextColumn "Inherited %alloc" (show.(/10).fromIntegral.inheritedAlloc) -- this action clears the tree model and then populates it with the -- profile contained in the profileVar, taking into account the current -- threshold value kept in the thresholdVar let repopulateTreeStore = do profile <- readIORef profileVar maybe (return ()) repopulateTreeStore' profile repopulateTreeStore' profile = do New.treeStoreClear store titleLabel `labelSetText` (title profile) commandLabel `labelSetText` (command profile) totalTimeLabel `labelSetText` (show (totalTime profile) ++ " sec") totalAllocLabel `labelSetText` (formatNumber (totalAlloc profile) ++ " bytes") threshold <- readIORef thresholdVar let node = if threshold > 0 then pruneOnThreshold threshold (breakdown profile) else Just (breakdown profile) toTree :: ProfileNode -> Tree.Tree ProfileNode toTree = Tree.unfoldTree (\node -> (node, children node)) case node of Nothing -> return () Just node -> New.treeStoreInsertTree store [] 0 (toTree node) -- associate actions with the menus -- the open menu item, opens a file dialog and then loads and displays -- the the profile (unless the user cancleled the dialog) openMenuItem <- xmlGetWidget dialogXml castToMenuItem "openMenuItem" openMenuItem `onActivateLeaf` do filename <- openFileDialog mainWindow when (isJust filename) (do profile <- parseProfileFile (fromJust filename) writeIORef profileVar (Just profile) repopulateTreeStore) quitMenuItem <- xmlGetWidget dialogXml castToMenuItem "quitMenuItem" quitMenuItem `onActivateLeaf` mainQuit aboutMenuItem <- xmlGetWidget dialogXml castToMenuItem "aboutMenuItem" aboutMenuItem `onActivateLeaf` showAboutDialog mainWindow -- each menu item in the "View" menu sets the thresholdVar and re-displays -- the current profile let doThresholdMenuItem threshold itemName = do menuItem <- xmlGetWidget dialogXml castToMenuItem itemName menuItem `onActivateLeaf` do writeIORef thresholdVar threshold repopulateTreeStore mapM_ (uncurry doThresholdMenuItem) [(0, "allEntries"), (1, "0.1%Entries"), (5, "0.5%Entries"), (10, "1%Entries"), (50, "5%Entries"), (100, "10%Entries"), (500, "50%Entries")] -- Check the command line to see if a profile file was given commands <- getArgs when (not (null commands)) (do profile <- parseProfileFile (head commands) writeIORef profileVar (Just profile) repopulateTreeStore) -- The final step is to display the main window and run the main loop widgetShowAll mainWindow mainGUI -- display a standard file open dialog openFileDialog :: Window -> IO (Maybe String) openFileDialog parentWindow = do dialog <- fileChooserDialogNew (Just "Open Profile... ") (Just parentWindow) FileChooserActionOpen [("gtk-cancel", ResponseCancel) ,("gtk-open", ResponseAccept)] widgetShow dialog response <- dialogRun dialog widgetHide dialog case response of ResponseAccept -> fileChooserGetFilename dialog _ -> return Nothing -- just to display a number using thousand seperators -- eg "3,456,235,596" formatNumber :: Integer -> String formatNumber = reverse . concat . intersperse "," . unfoldr (\l -> case splitAt 3 l of ([], _) -> Nothing p -> Just p) . reverse . show showAboutDialog :: Window -> IO () showAboutDialog parent = do -- create the about dialog aboutDialog <- aboutDialogNew -- set some attributes set aboutDialog [ aboutDialogName := "profileviewer", aboutDialogVersion := "0.2", aboutDialogCopyright := "Duncan Coutts", aboutDialogComments := "A viewer for GHC time profiles.", aboutDialogWebsite := "http://haskell.org/gtk2hs/" ] -- make the about dialog appear above the main window windowSetTransientFor aboutDialog parent -- make the dialog non-modal. When the user closes the dialog destroy it. afterResponse aboutDialog $ \_ -> widgetDestroy aboutDialog widgetShow aboutDialog