gcmole.lua 11.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
-- Copyright 2011 the V8 project authors. All rights reserved.
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions are
-- met:
--
--     * Redistributions of source code must retain the above copyright
--       notice, this list of conditions and the following disclaimer.
--     * Redistributions in binary form must reproduce the above
--       copyright notice, this list of conditions and the following
--       disclaimer in the documentation and/or other materials provided
--       with the distribution.
--     * Neither the name of Google Inc. nor the names of its
--       contributors may be used to endorse or promote products derived
--       from this software without specific prior written permission.
--
-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-- This is main driver for gcmole tool. See README for more details.
-- Usage: CLANG_BIN=clang-bin-dir lua tools/gcmole/gcmole.lua [arm|ia32|x64]

local DIR = arg[0]:match("^(.+)/[^/]+$")
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69

local FLAGS = {
   -- Do not build gcsuspects file and reuse previously generated one.
   reuse_gcsuspects = false;

   -- Print commands to console before executing them.
   verbose = false;

   -- Perform dead variable analysis (generates many false positives).
   -- TODO add some sort of whiteliste to filter out false positives.
   dead_vars = false;

   -- When building gcsuspects whitelist certain functions as if they
   -- can be causing GC. Currently used to reduce number of false
   -- positives in dead variables analysis. See TODO for WHITELIST
   -- below.
   whitelist = true;
}
local ARGS = {}

for i = 1, #arg do
   local flag = arg[i]:match "^%-%-([%w_-]+)$"
   if flag then
      local no, real_flag = flag:match "^(no)([%w_-]+)$"
      if real_flag then flag = real_flag end

      flag = flag:gsub("%-", "_")
      if FLAGS[flag] ~= nil then
         FLAGS[flag] = (no ~= "no")
      else
         error("Unknown flag: " .. flag)
      end
   else
      table.insert(ARGS, arg[i])
   end
end

local ARCHS = ARGS[1] and { ARGS[1] } or { 'ia32', 'arm', 'x64' }
70 71 72 73 74 75 76 77 78 79 80 81

local io = require "io"
local os = require "os"

function log(...)
   io.stderr:write(string.format(...))
   io.stderr:write "\n"
end

-------------------------------------------------------------------------------
-- Clang invocation

82
local CLANG_BIN = os.getenv "CLANG_BIN"
83 84 85

if not CLANG_BIN or CLANG_BIN == "" then
   error "CLANG_BIN not set"
86
end
87

88 89 90 91 92 93 94 95
local function MakeClangCommandLine(plugin, plugin_args, triple, arch_define)
   if plugin_args then
     for i = 1, #plugin_args do
        plugin_args[i] = "-plugin-arg-" .. plugin .. " " .. plugin_args[i]
     end
     plugin_args = " " .. table.concat(plugin_args, " ")
   end
   return CLANG_BIN .. "/clang -cc1 -load " .. DIR .. "/libgcmole.so"
96
      .. " -plugin "  .. plugin
97 98
      .. (plugin_args or "")
      .. " -triple " .. triple
99 100 101 102 103 104 105
      .. " -D" .. arch_define
      .. " -DENABLE_DEBUGGER_SUPPORT"
      .. " -Isrc"
end

function InvokeClangPluginForEachFile(filenames, cfg, func)
   local cmd_line = MakeClangCommandLine(cfg.plugin,
106 107 108
                                         cfg.plugin_args,
                                         cfg.triple,
                                         cfg.arch_define)
109

110
   for _, filename in ipairs(filenames) do
111 112
      log("-- %s", filename)
      local action = cmd_line .. " src/" .. filename .. " 2>&1"
113
      if FLAGS.verbose then print('popen ', action) end
114 115 116 117 118 119 120 121 122 123 124 125 126 127
      local pipe = io.popen(action)
      func(filename, pipe:lines())
      pipe:close()
   end
end

-------------------------------------------------------------------------------
-- SConscript parsing

local function ParseSConscript()
   local f = assert(io.open("src/SConscript"), "failed to open SConscript")
   local sconscript = f:read('*a')
   f:close()

128
   local SOURCES = sconscript:match "SOURCES = {(.-)}";
129 130 131 132 133 134 135 136

   local sources = {}

   for condition, list in
      SOURCES:gmatch "'([^']-)': Split%(\"\"\"(.-)\"\"\"%)" do
      local files = {}
      for file in list:gmatch "[^%s]+" do table.insert(files, file) end
      sources[condition] = files
137
   end
138 139 140 141 142

   for condition, list in SOURCES:gmatch "'([^']-)': %[(.-)%]" do
      local files = {}
      for file in list:gmatch "'([^']-)'" do table.insert(files, file) end
      sources[condition] = files
143
   end
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162

   return sources
end

local function EvaluateCondition(cond, props)
   if cond == 'all' then return true end

   local p, v = cond:match "(%w+):(%w+)"

   assert(p and v, "failed to parse condition: " .. cond)
   assert(props[p] ~= nil, "undefined configuration property: " .. p)

   return props[p] == v
end

local function BuildFileList(sources, props)
   local list = {}
   for condition, files in pairs(sources) do
      if EvaluateCondition(condition, props) then
163
         for i = 1, #files do table.insert(list, files[i]) end
164 165 166 167 168 169 170 171 172
      end
   end
   return list
end

local sources = ParseSConscript()

local function FilesForArch(arch)
   return BuildFileList(sources, { os = 'linux',
173 174 175
                                   arch = arch,
                                   mode = 'debug',
                                   simulator = ''})
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
end

local mtConfig = {}

mtConfig.__index = mtConfig

local function config (t) return setmetatable(t, mtConfig) end

function mtConfig:extend(t)
   local e = {}
   for k, v in pairs(self) do e[k] = v end
   for k, v in pairs(t) do e[k] = v end
   return config(e)
end

local ARCHITECTURES = {
   ia32 = config { triple = "i586-unknown-linux",
193
                   arch_define = "V8_TARGET_ARCH_IA32" },
194
   arm = config { triple = "i586-unknown-linux",
195
                  arch_define = "V8_TARGET_ARCH_ARM" },
196
   x64 = config { triple = "x86_64-unknown-linux",
197
                  arch_define = "V8_TARGET_ARCH_X64" }
198 199 200
}

-------------------------------------------------------------------------------
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
-- GCSuspects Generation

local gc, gc_caused, funcs

local WHITELIST = {
   -- The following functions call CEntryStub which is always present.
   "MacroAssembler.*CallExternalReference",
   "MacroAssembler.*CallRuntime",
   "CompileCallLoadPropertyWithInterceptor",
   "CallIC.*GenerateMiss",

   -- DirectCEntryStub is a special stub used on ARM. 
   -- It is pinned and always present.
   "DirectCEntryStub.*GenerateCall",  

   -- TODO GCMole currently is sensitive enough to understand that certain 
   --      functions only cause GC and return Failure simulataneously. 
   --      Callsites of such functions are safe as long as they are properly 
   --      check return value and propagate the Failure to the caller.
   --      It should be possible to extend GCMole to understand this.
   "Heap.*AllocateFunctionPrototype"
};

local function AddCause(name, cause)
   local t = gc_caused[name]
   if not t then
      t = {}
      gc_caused[name] = t
   end
   table.insert(t, cause)
end
232 233 234

local function resolve(name)
   local f = funcs[name]
235 236

   if not f then
237 238
      f = {}
      funcs[name] = f
239 240 241 242 243 244 245 246 247 248 249 250 251

      if name:match "Collect.*Garbage" then
         gc[name] = true
         AddCause(name, "<GC>")
      end

      if FLAGS.whitelist then
         for i = 1, #WHITELIST do
            if name:match(WHITELIST[i]) then
               gc[name] = false
            end
         end
      end
252
   end
253

254 255 256 257 258 259 260 261
    return f
end

local function parse (filename, lines)
   local scope

   for funcname in lines do
      if funcname:sub(1, 1) ~= '\t' then
262 263
         resolve(funcname)
         scope = funcname
264
      else
265 266
         local name = funcname:sub(2)
         resolve(name)[scope] = true
267 268 269 270 271 272 273
      end
   end
end

local function propagate ()
   log "** Propagating GC information"

274 275 276 277 278 279 280
   local function mark(from, callers)
      for caller, _ in pairs(callers) do
         if gc[caller] == nil then
            gc[caller] = true
            mark(caller, funcs[caller])
         end
         AddCause(caller, from)
281 282 283 284
      end
   end

   for funcname, callers in pairs(funcs) do
285
      if gc[funcname] then mark(funcname, callers) end
286 287 288 289
   end
end

local function GenerateGCSuspects(arch, files, cfg)
290 291 292
   -- Reset the global state.
   gc, gc_caused, funcs = {}, {}, {}

293 294 295 296
   log ("** Building GC Suspects for %s", arch)
   InvokeClangPluginForEachFile (files,
                                 cfg:extend { plugin = "dump-callees" },
                                 parse)
297

298 299 300
   propagate()

   local out = assert(io.open("gcsuspects", "w"))
301 302 303 304 305 306 307 308 309 310 311
   for name, value in pairs(gc) do if value then out:write (name, '\n') end end
   out:close()

   local out = assert(io.open("gccauses", "w"))
   out:write "GC = {"
   for name, causes in pairs(gc_caused) do
      out:write("['", name, "'] = {")
      for i = 1, #causes do out:write ("'", causes[i], "';") end
      out:write("};\n")
   end
   out:write "}"
312
   out:close()
313

314 315 316
   log ("** GCSuspects generated for %s", arch)
end

317
--------------------------------------------------------------------------------
318 319
-- Analysis

320
local function CheckCorrectnessForArch(arch)
321 322 323
   local files = FilesForArch(arch)
   local cfg = ARCHITECTURES[arch]

324 325 326
   if not FLAGS.reuse_gcsuspects then
      GenerateGCSuspects(arch, files, cfg)
   end
327 328 329 330 331 332

   local processed_files = 0
   local errors_found = false
   local function SearchForErrors(filename, lines)
      processed_files = processed_files + 1
      for l in lines do
333 334 335 336
         errors_found = errors_found or
            l:match "^[^:]+:%d+:%d+:" or
            l:match "error" or
            l:match "warning"
337 338 339 340
         print(l)
      end
   end

341 342 343 344 345
   log("** Searching for evaluation order problems%s for %s",
       FLAGS.dead_vars and " and dead variables" or "",
       arch)
   local plugin_args
   if FLAGS.dead_vars then plugin_args = { "--dead-vars" } end
346
   InvokeClangPluginForEachFile(files,
347 348 349
                                cfg:extend { plugin = "find-problems",
                                             plugin_args = plugin_args },
                                SearchForErrors)
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
   log("** Done processing %d files. %s",
       processed_files,
       errors_found and "Errors found" or "No errors found")

   return errors_found
end

local function SafeCheckCorrectnessForArch(arch)
   local status, errors = pcall(CheckCorrectnessForArch, arch)
   if not status then
      print(string.format("There was an error: %s", errors))
      errors = true
   end
   return errors
end

local errors = false

for _, arch in ipairs(ARCHS) do
   if not ARCHITECTURES[arch] then
      error ("Unknown arch: " .. arch)
   end

   errors = SafeCheckCorrectnessForArch(arch, report) or errors
end

os.exit(errors and 1 or 0)