helper

local ffi = require('ffi')

-- Define basic ctypes
ffi.cdef [[
	struct bpf_insn {
	  uint8_t code;   /* opcode */
	  uint8_t dst_reg:4;  /* dest register */
	  uint8_t src_reg:4;  /* source register */
	  uint16_t off;   /* signed offset */
	  uint32_t imm;   /* signed immediate constant */
	};
]]

-- Inject mock ljsyscall for tests
package.loaded['syscall'] = {
	bpf = function() error('mock') end,
	c = { BPF_MAP = {}, BPF_PROG = {} },
	abi = { arch = 'x64' },
}

package.loaded['syscall.helpers'] = {
	strflag = function (tab)
		local function flag(cache, str)
			if type(str) ~= "string" then return str end
			if #str == 0 then return 0 end
			local s = str:upper()
			if #s == 0 then return 0 end
			local val = rawget(tab, s)
			if not val then return nil end
			cache[str] = val
			return val
		end
		return setmetatable(tab, {__index = setmetatable({}, {__index = flag}), __call = function(t, a) return t[a] end})
	end
}


local ffi = require('ffi')
local S = require('syscall')
-- Normalize whitespace and remove empty lines
	local res = {}
	for line in string.gmatch(c,'[^\r\n]+') do
		local op, d, s, t = line:match('(%S+)%s+(%S+)%s+(%S+)%s*([^-]*)')
		if op then
			t = t and t:match('^%s*(.-)%s*$')
			table.insert(res, string.format('%s\t%s %s %s', op, d, s, t))
		end
	end
	return table.concat(res, '\n')
end
-- Compile code and check result
local function compile(t)
	local bpf = require('bpf')
	-- require('jit.bc').dump(t.input)
	local code, err = bpf(t.input)
	assert.truthy(code)
	assert.falsy(err)
	if code then
		if t.expect then
			local got = normalize_code(bpf.dump_string(code, 1, true))
			-- if normalize_code(t.expect) ~= got then print(bpf.dump_string(code, 1)) end
			assert.same(normalize_code(t.expect), got)
		end
	end
end
-- Make a mock map variable
local function makemap(type, max_entries, key_ctype, val_ctype)
	if not key_ctype then key_ctype = ffi.typeof('uint32_t') end
	if not val_ctype then val_ctype = ffi.typeof('uint32_t') end
	if not max_entries then max_entries = 4096 end
	return {
		__map = true,
		max_entries = max_entries,
		key = ffi.new(ffi.typeof('$ [1]', key_ctype)),
		val = ffi.new(ffi.typeof('$ [1]', val_ctype)),
		map_type = S.c.BPF_MAP[type],
		key_type = key_ctype,
		val_type = val_ctype,
		fd = 42,
	}
end
describe('codegen', function()
	-- luacheck: ignore 113 211 212 311 511
	describe('constants', function()
		it('remove dead constant store', function()
			compile {
				input = function ()
					local proto = 5
				end,
				expect = [[
					MOV		R0	#0
					EXIT	R0	#0
				]]
			}
		end)
		it('materialize constant', function()
			compile {
				input = function ()
					return 5
				end,
				expect = [[
					MOV		R0	#5
					EXIT	R0	#0
				]]
			}
		end)
		it('materialize constant longer than i32', function()
			compile {
				input = function ()
					return 4294967295
				end,
				expect = [[
					LDDW	R0	#4294967295
					EXIT	R0	#0
				]]
			}
		end)
		it('materialize cdata constant', function()
			compile {
				input = function ()
					return 5ULL
				end,
				expect = [[
					LDDW	R0	#5 -- composed instruction
					EXIT	R0	#0
				]]
			}
		end)
		it('materialize signed cdata constant', function()
			compile {
				input = function ()
					return 5LL
				end,
				expect = [[
					LDDW	R0	#5 -- composed instruction
					EXIT	R0	#0
				]]
			}
		end)
		it('materialize coercible numeric cdata constant', function()
			compile {
				input = function ()
					return 0x00005
				end,
				expect = [[
					MOV		R0	#5
					EXIT	R0	#0
				]]
			}
		end)
		it('materialize constant through variable', function()
		compile {
			input = function ()
				local proto = 5
				return proto
			end,
			expect = [[
				MOV		R0	#5
				EXIT	R0	#0
			]]
		}
		end)
		it('eliminate constant expressions', function()
			compile {
				input = function ()
					return 2 + 3 - 0
				end,
				expect = [[
					MOV		R0	#5
					EXIT	R0	#0
				]]
			}
		end)
		it('eliminate constant expressions (if block)', function()
			compile {
				input = function ()
					local proto = 5
					if proto == 5 then
						proto = 1
					end
					return proto
				end,
				expect = [[
					MOV		R0	#1
					EXIT	R0	#0
				]]
			}
		end)
		it('eliminate negative constant expressions (if block) NYI', function()
			-- always negative condition is not fully eliminated
			compile {
				input = function ()
					local proto = 5
					if false then
						proto = 1
					end
					return proto
				end,
				expect = [[
					MOV		R7		#5
					STXDW	[R10-8] R7
					MOV		R7		#0
					JEQ		R7		#0 => 0005
					LDXDW	R0 		[R10-8]
					EXIT	R0		#0
				]]
			}
		end)
	end)
	describe('variables', function()
		it('classic packet access (fold constant offset)', function()
			compile {
				input = function (skb)
					return eth.ip.tos -- constant expression will fold
				end,
				expect = [[
					LDB		R0	skb[15]
					EXIT	R0	#0
				]]
			}
		end)
		it('classic packet access (load non-constant offset)', function()
			compile {
				input = function (skb)
					return eth.ip.udp.src_port -- need to skip variable-length header
				end,
				expect = [[
					LDB		R0			skb[14]
					AND		R0			#15
					LSH		R0			#2
					ADD		R0 			#14
					STXDW	[R10-16]	R0 -- NYI: erase dead store
					LDH		R0 			skb[R0+0]
					END		R0 			R0
					EXIT	R0 			#0
				]]
			}
		end)
		it('classic packet access (manipulate dissector offset)', function()
			compile {
				input = function (skb)
					local ptr = eth.ip.udp.data + 1
					return ptr[0] -- dereference dissector pointer
				end,
				expect = [[
					LDB		R0			skb[14]
					AND		R0			#15
					LSH		R0			#2
					ADD		R0			#14 -- NYI: fuse commutative operations in second pass
					ADD		R0			#8
					ADD		R0			#1
					STXDW	[R10-16] 	R0
					LDB		R0			skb[R0+0]
					EXIT	R0			#0
				]]
			}
		end)
		it('classic packet access (multi-byte load)', function()
			compile {
				input = function (skb)
					local ptr = eth.ip.udp.data
					return ptr(1, 5) -- load 4 bytes
				end,
				expect = [[
					LDB		R0			skb[14]
					AND		R0			#15
					LSH		R0			#2
					ADD		R0			#14
					ADD		R0			#8
					MOV		R7			R0
					STXDW	[R10-16]	R0 -- NYI: erase dead store
					LDW		R0			skb[R7+1]
					END		R0			R0
					EXIT	R0			#0
				]]
			}
		end)
		it('direct skb field access', function()
			compile {
				input = function (skb)
					return skb.len
				end,
				expect = [[
					LDXW	R7	[R6+0]
					MOV		R0	R7
					EXIT	R0	#0
				]]
			}
		end)
		it('direct skb data access (manipulate offset)', function()
			compile {
				input = function (skb)
					local ptr = skb.data + 5
					return ptr[0]
				end,
				expect = [[
					LDXW	R7	[R6+76]
					ADD		R7	#5
					LDXB	R8 	[R7+0] -- NYI: transform LD + ADD to LD + offset addressing
					MOV		R0 	R8
					EXIT	R0	#0
				]]
			}
		end)
		it('direct skb data access (offset boundary check)', function()
			compile {
				input = function (skb)
					local ptr = skb.data + 5
					if ptr < skb.data_end then
						return ptr[0]
					end
				end,
				expect = [[
					LDXW	R7	[R6+76]
					ADD		R7	#5
					LDXW	R8	[R6+80]
					JGE		R7	R8 => 0008
					LDXB	R8	[R7+0]
					MOV		R0 	R8
					EXIT	R0	#0
					MOV		R0	#0
					EXIT	R0	#0
				]]
			}
		end)
		it('access stack memory (array, const load, const store)', function()
			compile {
				input = function (skb)
					local mem = ffi.new('uint8_t [16]')
					mem[0] = 5
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-40] 	R0
					STXDW	[R10-48] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					STB		[R10-48] 	#5
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('access stack memory (array, const load, packet store)', function()
			compile {
				input = function (skb)
					local mem = ffi.new('uint8_t [7]')
					mem[0] = eth.ip.tos
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-40] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					LDB		R0 			skb[15]
					STXB	[R10-40] 	R0
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('access stack memory (array, packet load, const store)', function()
			compile {
				input = function (skb)
					local mem = ffi.new('uint8_t [1]')
					mem[eth.ip.tos] = 5
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-48] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					LDB		R0 			skb[15]
					MOV		R7 			R0
					ADD		R7 			R10
					STB		[R7-48] 	#5
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('access stack memory (array, packet load, packet store)', function()
			compile {
				input = function (skb)
					local mem = ffi.new('uint8_t [7]')
					local v = eth.ip.tos
					mem[v] = v
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-40] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					LDB		R0 			skb[15]
					MOV		R7 			R0
					ADD		R7 			R10
					STXB	[R7-40] 	R0
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('access stack memory (struct, const/packet store)', function()
			local kv_t = 'struct { uint64_t a; uint64_t b; }'
			compile {
				input = function (skb)
					local mem = ffi.new(kv_t)
					mem.a = 5
					mem.b = eth.ip.tos
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-40] 	R0
					STXDW	[R10-48] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					MOV		R7 			#5
					STXDW	[R10-48] 	R7
					LDB		R0 			skb[15]
					STXDW	[R10-40] 	R0
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('access stack memory (struct, const/stack store)', function()
			local kv_t = 'struct { uint64_t a; uint64_t b; }'
			compile {
				input = function (skb)
					local m1 = ffi.new(kv_t)
					local m2 = ffi.new(kv_t)
					m1.a = 5
					m2.b = m1.a
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-48] 	R0
					STXDW	[R10-56] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					MOV		R0 			#0
					STXDW	[R10-64] 	R0
					STXDW	[R10-72] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					MOV		R7 			#5
					STXDW	[R10-56] 	R7
					LDXDW	R7 			[R10-56]
					STXDW	[R10-64] 	R7
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (u32, const key load)', function()
			local array_map = makemap('array', 256)
			compile {
				input = function (skb)
					return array_map[0]
				end,
				expect = [[
					LDDW	R1			#42
					STW		[R10-28]	#0
					MOV		R2			R10
					ADD		R2			#4294967268
					CALL	R0			#1 ; map_lookup_elem
					JEQ		R0			#0 => 0009
					LDXW	R0			[R0+0]
					EXIT	R0			#0
				]]
			}
		end)
		it('array map (u32, packet key load)', function()
			local array_map = makemap('array', 256)
			compile {
				input = function (skb)
					return array_map[eth.ip.tos]
				end,
				expect = [[
					LDB 	R0 			skb[15]
					LDDW	R1			#42
					STXW	[R10-36] 	R0
					MOV		R2			R10
					ADD		R2			#4294967260
					STXDW	[R10-24] 	R0 -- NYI: erase dead store
					CALL	R0			#1 ; map_lookup_elem
					JEQ		R0			#0 => 0011
					LDXW	R0			[R0+0]
					EXIT	R0			#0
				]]
			}
		end)
		it('array map (u32, const key store, const value)', function()
			local array_map = makemap('array', 256)
			compile {
				input = function (skb)
					array_map[0] = 5
				end,
				expect = [[
					LDDW	R1 			#42
					STW		[R10-36] 	#0
					MOV		R2 			R10
					ADD		R2 			#4294967260
					MOV		R4 			#0
					STW		[R10-40] 	#5
					MOV		R3 			R10
					ADD		R3 			#4294967256
					CALL	R0 			#2 ; map_update_elem
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (u32, const key store, packet value)', function()
			local array_map = makemap('array', 256)
			compile {
				input = function (skb)
					array_map[0] = eth.ip.tos
				end,
				expect = [[
					LDB		R0 			skb[15]
					STXDW	[R10-24] 	R0
					LDDW	R1 			#42
					STW		[R10-36] 	#0
					MOV		R2 			R10
					ADD		R2 			#4294967260
					MOV		R4 			#0
					MOV		R3 			R10
					ADD		R3 			#4294967272
					CALL	R0 			#2 ; map_update_elem
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (u32, const key store, map value)', function()
			local array_map = makemap('array', 256)
			compile {
				input = function (skb)
					array_map[0] = array_map[1]
				end,
				expect = [[
					LDDW	R1 			#42
					STW		[R10-36] 	#1
					MOV		R2 			R10
					ADD		R2 			#4294967260
					CALL	R0 			#1 ; map_lookup_elem
					STXDW	[R10-24] 	R0
					LDDW	R1 			#42
					STW		[R10-36]	#0
					MOV		R2			R10
					ADD		R2			#4294967260
					MOV		R4			#0
					LDXDW	R3			[R10-24]
					JEQ		R3			#0 => 0017
					LDXW	R3			[R3+0]
					STXW	[R10-40]	R3
					MOV		R3 			R10
					ADD		R3 			#4294967256
					CALL	R0 			#2 ; map_update_elem
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (u32, const key replace, const value)', function()
			local array_map = makemap('array', 256)
			compile {
				input = function (skb)
					local val = array_map[0]
					if val then
						val[0] = val[0] + 1
					else
						array_map[0] = 5
					end
				end,
				expect = [[
					LDDW	R1 			#42
					STW		[R10-44] 	#0
					MOV		R2 			R10
					ADD		R2 			#4294967252
					CALL	R0 			#1 ; map_lookup_elem
					JEQ		R0 			#0 => 0013 -- if (map_value ~= NULL)
					LDXW	R7 			[R0+0]
					ADD		R7 			#1
					STXW	[R0+0] 		R7
					MOV		R7 			#0
					JEQ		R7 			#0 => 0025 -- skip false branch
					STXDW	[R10-16] 	R0
					LDDW	R1 			#42
					STW		[R10-44] 	#0
					MOV		R2 			R10
					ADD		R2 			#4294967252
					MOV		R4 			#0
					STW		[R10-48] 	#5
					MOV		R3 			R10
					ADD		R3 			#4294967248
					CALL	R0 			#2 ; map_update_elem
					LDXDW	R0 			[R10-16]
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (u32, const key replace xadd, const value)', function()
			local array_map = makemap('array', 256)
			compile {
				input = function (skb)
					local val = array_map[0]
					if val then
						xadd(val, 1)
					else
						array_map[0] = 5
					end
				end,
				expect = [[
					LDDW	R1 			#42
					STW		[R10-52] 	#0
					MOV		R2 			R10
					ADD		R2 			#4294967244
					CALL	R0 			#1 ; map_lookup_elem
					JEQ		R0 			#0 => 0014 -- if (map_value ~= NULL)
					MOV		R7 			#1
					MOV		R8 			R0
					STXDW	[R10-16] 	R0
					XADDW	[R8+0] 		R7
					MOV		R7 			#0
					JEQ		R7 			#0 => 0025 -- skip false branch
					STXDW	[R10-16] 	R0
					LDDW	R1 			#42
					STW		[R10-52] 	#0
					MOV		R2 			R10
					ADD		R2 			#4294967244
					MOV		R4 			#0
					STW		[R10-56] 	#5
					MOV		R3 			R10
					ADD		R3 			#4294967240
					CALL	R0 			#2 ; map_update_elem
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (u32, const key replace xadd, const value) inverse nil check', function()
			local array_map = makemap('array', 256)
			compile {
				input = function (skb)
					local val = array_map[0]
					if not val then
						array_map[0] = 5
					else
						xadd(val, 1)
					end
				end,
				expect = [[
					LDDW	R1 			#42
					STW		[R10-52] 	#0
					MOV		R2 			R10
					ADD		R2 			#4294967244
					CALL	R0 			#1 ; map_lookup_elem
					JNE		R0 			#0 => 0021
					STXDW	[R10-16] 	R0
					LDDW	R1 			#42
					STW		[R10-52] 	#0
					MOV		R2 			R10
					ADD		R2 			#4294967244
					MOV		R4 			#0
					STW		[R10-56] 	#5
					MOV		R3 			R10
					ADD		R3 			#4294967240
					CALL	R0 			#2 ; map_update_elem
					MOV		R7 			#0
					JEQ		R7 			#0 => 0025
					MOV		R7 			#1
					MOV		R8 			R0
					STXDW	[R10-16] 	R0
					XADDW	[R8+0] 		R7
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (struct, stack key load)', function()
			local kv_t = 'struct { uint64_t a; uint64_t b; }'
			local array_map = makemap('array', 256, ffi.typeof(kv_t), ffi.typeof(kv_t))
			compile {
				input = function (skb)
					local key = ffi.new(kv_t)
					key.a = 2
					key.b = 3
					local val = array_map[key] -- Use composite key from stack memory
					if val then
						return val.a
					end
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-48] 	R0
					STXDW	[R10-56] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					MOV		R7 			#2
					STXDW	[R10-56] 	R7
					MOV		R7 			#3
					STXDW	[R10-48] 	R7
					LDDW	R1			#42
					MOV		R2			R10
					ADD		R2			#4294967240
					CALL	R0			#1 ; map_lookup_elem
					JEQ		R0 			#0 => 0017
					LDXDW	R7 			[R0+0]
					MOV		R0 			R7
					EXIT	R0 			#0
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (struct, stack key store)', function()
			local kv_t = 'struct { uint64_t a; uint64_t b; }'
			local array_map = makemap('array', 256, ffi.typeof(kv_t), ffi.typeof(kv_t))
			compile {
				input = function (skb)
					local key = ffi.new(kv_t)
					key.a = 2
					key.b = 3
					array_map[key] = key -- Use composite key from stack memory
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-40] 	R0
					STXDW	[R10-48] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					MOV		R7 			#2
					STXDW	[R10-48] 	R7
					MOV		R7 			#3
					STXDW	[R10-40] 	R7
					LDDW	R1 			#42
					MOV		R2 			R10
					ADD		R2 			#4294967248
					MOV		R4 			#0
					MOV		R3 			R10
					ADD		R3 			#4294967248
					CALL	R0 			#2 ; map_update_elem
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (struct, stack/packet key update, const value)', function()
			local kv_t = 'struct { uint64_t a; uint64_t b; }'
			local array_map = makemap('array', 256, ffi.typeof(kv_t), ffi.typeof(kv_t))
			compile {
				input = function (skb)
					local key = ffi.new(kv_t)
					key.a = eth.ip.tos   -- Load key part from dissector
					local val = array_map[key]
					if val then
						val.a = 5
					end
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-48] 	R0
					STXDW	[R10-56] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					LDB		R0 			skb[15]
					STXDW	[R10-56] 	R0
					LDDW	R1			#42
					MOV		R2			R10
					ADD		R2			#4294967240
					CALL	R0			#1 ; map_lookup_elem
					JEQ		R0 			#0 => 0014
					MOV		R7 			#5
					STXDW	[R0+0] 		R7
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (struct, stack/packet key update, map value)', function()
			local kv_t = 'struct { uint64_t a; uint64_t b; }'
			local array_map = makemap('array', 256, ffi.typeof(kv_t), ffi.typeof(kv_t))
			compile {
				input = function (skb)
					local key = ffi.new(kv_t)
					key.a = eth.ip.tos   -- Load key part from dissector
					local val = array_map[key]
					if val then
						val.a = val.b
					end
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-48] 	R0
					STXDW	[R10-56] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					LDB		R0 			skb[15]
					STXDW	[R10-56] 	R0
					LDDW	R1			#42
					MOV		R2			R10
					ADD		R2			#4294967240
					CALL	R0			#1 ; map_lookup_elem
					JEQ		R0 			#0 => 0014
					LDXDW	R7 			[R0+8]
					STXDW	[R0+0] 		R7
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (struct, stack/packet key update, stack value)', function()
			local kv_t = 'struct { uint64_t a; uint64_t b; }'
			local array_map = makemap('array', 256, ffi.typeof(kv_t), ffi.typeof(kv_t))
			compile {
				input = function (skb)
					local key = ffi.new(kv_t)
					key.a = eth.ip.tos   -- Load key part from dissector
					local val = array_map[key]
					if val then
						val.a = key.b
					end
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-48] 	R0
					STXDW	[R10-56] 	R0 -- NYI: erase zero-fill on allocation when it's loaded later
					LDB		R0 			skb[15]
					STXDW	[R10-56] 	R0
					LDDW	R1			#42
					MOV		R2			R10
					ADD		R2			#4294967240
					CALL	R0			#1 ; map_lookup_elem
					JEQ		R0 			#0 => 0014
					LDXDW	R7 			[R10-48]
					STXDW	[R0+0] 		R7
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
		it('array map (struct, stack/packet key replace, stack value)', function()
			local kv_t = 'struct { uint64_t a; uint64_t b; }'
			local array_map = makemap('array', 256, ffi.typeof(kv_t), ffi.typeof(kv_t))
			compile {
				input = function (skb)
					local key = ffi.new(kv_t)
					key.a = eth.ip.tos   -- Load key part from dissector
					local val = array_map[key]
					if val then
						val.a = key.b
					else
						array_map[key] = key
					end
				end,
				expect = [[
					MOV		R0 			#0
					STXDW	[R10-48] 	R0
					STXDW	[R10-56] 	R0
					LDB		R0 			skb[15]
					STXDW	[R10-56] 	R0
					LDDW	R1 			#42
					MOV		R2 			R10
					ADD		R2 			#4294967240
					CALL	R0 			#1 ; map_lookup_elem
					JEQ		R0 			#0 => 0016 -- if (map_value ~= NULL)
					LDXDW	R7 			[R10-48]
					STXDW	[R0+0] 		R7
					MOV		R7 			#0
					JEQ		R7 			#0 => 0026 -- jump over false branch
					STXDW	[R10-24] 	R0
					LDDW	R1 			#42
					MOV		R2 			R10
					ADD		R2 			#4294967240
					MOV		R4 			#0
					MOV		R3 			R10
					ADD		R3 			#4294967240
					CALL	R0 			#2 ; map_update_elem
					LDXDW	R0 			[R10-24]
					MOV		R0 			#0
					EXIT	R0 			#0
				]]
			}
		end)
	end)
	describe('control flow', function()
		it('condition with constant return', function()
			compile {
				input = function (skb)
					local v = eth.ip.tos
					if v then
						return 1
					else
						return 0
					end
				end,
				expect = [[
					LDB		R0 			skb[15]
					JEQ		R0 			#0 => 0005
					MOV		R0 			#1
					EXIT	R0 			#0
					MOV		R0 			#0 -- 0005 jump target
					EXIT	R0 			#0
				]]
			}
		end)
		it('condition with cdata constant return', function()
			local cdata = 2ULL
			compile {
				input = function (skb)
					local v = eth.ip.tos
					if v then
						return cdata + 1
					else
						return 0
					end
				end,
				expect = [[
					LDB		R0 			skb[15]
					JEQ		R0 			#0 => 0006
					LDDW	R0 			#3
					EXIT	R0 			#0
					MOV		R0 			#0 -- 0006 jump target
					EXIT	R0 			#0
				]]
			}
		end)
		it('condition with constant return (inversed)', function()
			compile {
				input = function (skb)
					local v = eth.ip.tos
					if not v then
						return 1
					else
						return 0
					end
				end,
				expect = [[
					LDB		R0 			skb[15]
					JNE		R0 			#0 => 0005
					MOV		R0 			#1
					EXIT	R0 			#0
					MOV		R0 			#0 -- 0005 jump target
					EXIT	R0 			#0
				]]
			}
		end)
		it('condition with variable mutation', function()
			compile {
				input = function (skb)
					local v = 0
					if eth.ip.tos then
						v = 1
					end
					return v
				end,
				expect = [[
					LDB		R0 			skb[15]
					MOV		R1 			#0
					STXDW	[R10-16] 	R1
					JEQ		R0 			#0 => 0007
					MOV		R7 			#1
					STXDW	[R10-16] 	R7
					LDXDW	R0 			[R10-16]
					EXIT	R0 			#0
				]]
			}
		end)
		it('condition with nil variable mutation', function()
			compile {
				input = function (skb)
					local v -- nil, will be elided
					if eth.ip.tos then
						v = 1
					else
						v = 0
					end
					return v
				end,
				expect = [[
					LDB		R0 			skb[15]
					JEQ		R0 			#0 => 0007
					MOV		R7 			#1
					STXDW	[R10-16] 	R7
					MOV		R7 			#0
					JEQ		R7 			#0 => 0009
					MOV		R7 			#0
					STXDW	[R10-16] 	R7
					LDXDW	R0 			[R10-16]
					EXIT	R0 			#0
				]]
			}
		end)
		it('nested condition with variable mutation', function()
			compile {
				input = function (skb)
					local v = 0
					local tos = eth.ip.tos
					if tos then
						if tos > 5 then
							v = 5
						else
							v = 1
						end
					end
					return v
				end,
				expect = [[
					LDB		R0 			skb[15]
					MOV		R1 			#0
					STXDW	[R10-16] 	R1 -- materialize v = 0
					JEQ		R0 			#0 => 0013 -- if not tos
					MOV		R7 			#5
					JGE		R7 			R0 => 0011 -- if 5 > tos
					MOV		R7 			#5
					STXDW	[R10-16] 	R7 -- materialize v = 5
					MOV		R7 			#0
					JEQ		R7 			#0 => 0013
					MOV		R7 			#1 -- 0011 jump target
					STXDW	[R10-16]	R7 -- materialize v = 1
					LDXDW	R0 			[R10-16]
					EXIT	R0 			#0
				]]
			}
		end)
		it('nested condition with variable shadowing', function()
			compile {
				input = function (skb)
					local v = 0
					local tos = eth.ip.tos
					if tos then
						local v = 0 -- luacheck: ignore 231
						if tos > 5 then
							v = 5 -- changing shadowing variable
						end
					else
						v = 1
					end
					return v
				end,
				expect = [[
					LDB		R0 			skb[15]
					MOV		R1 			#0
					STXDW	[R10-16] 	R1 -- materialize v = 0
					JEQ		R0 			#0 => 0011 -- if not tos
					MOV		R7 			#5
					MOV		R1 			#0
					STXDW	[R10-32] 	R1 -- materialize shadowing variable
					JGE		R7 			R0 => 0013 -- if 5 > tos
					MOV		R7 			#0 -- erased 'v = 5' dead store
					JEQ		R7 			#0 => 0013
					MOV		R7 			#1 -- 0011 jump target
					STXDW	[R10-16]	R7 -- materialize v = 1
					LDXDW	R0 			[R10-16] -- 0013 jump target
					EXIT	R0 			#0
				]]
			}
		end)
		it('condition materializes shadowing variable at the end of BB', function()
			compile {
				input = function (skb)
					local v = time()
					local v1 = 0 -- luacheck: ignore 231
					if eth.ip.tos then
						v1 = v
					end
				end,
				expect = [[
					CALL	R0 			#5 ; ktime_get_ns
					STXDW	[R10-16] 	R0
					LDB		R0 			skb[15]
					MOV		R1 			#0
					STXDW	[R10-24] 	R1 -- materialize v1 = 0
					JEQ		R0 			#0 => 0009
					LDXDW	R7 			[R10-16]
					STXDW	[R10-24] 	R7 -- v1 = v0
					MOV		R0 #0
					EXIT	R0 #0
				]]
			}
		end)
	end)
end)


squish

#!/usr/bin/env lua
local short_opts = { v = "verbose", vv = "very_verbose", o = "output", q = "quiet", qq = "very_quiet", g = "debug" }
local opts = { use_http = false };
for _, opt in ipairs(arg) do
	if opt:match("^%-") then
		local name = opt:match("^%-%-?([^%s=]+)()")
		name = (short_opts[name] or name):gsub("%-+", "_");
		if name:match("^no_") then
			name = name:sub(4, -1);
			opts[name] = false;
		else
			opts[name] = opt:match("=(.*)$") or true;
		end
	else
		base_path = opt;
	end
end
if opts.very_verbose then opts.verbose = true; end
if opts.very_quiet then opts.quiet = true; end
local noprint = function () end
local print_err, print_info, print_verbose, print_debug = noprint, noprint, noprint, noprint;
if not opts.very_quiet then print_err = print; end
if not opts.quiet then print_info = print; end
if opts.verbose or opts.very_verbose then print_verbose = print; end
if opts.very_verbose then print_debug = print; end
print = print_verbose;
local modules, main_files, resources = {}, {}, {};
--  Functions to be called from squishy file  --
function Module(name)
	if modules[name] then
		print_verbose("Ignoring duplicate module definition for "..name);
		return function () end
	end
	local i = #modules+1;
	modules[i] = { name = name, url = ___fetch_url };
	modules[name] = modules[i];
	return function (path)
		modules[i].path = path;
	end
end
function Resource(name, path)
	local i = #resources+1;
	resources[i] = { name = name, path = path or name };
	return function (path)
		resources[i].path = path;
	end
end
function AutoFetchURL(url)
	___fetch_url = url;
end
function Main(fn)
	table.insert(main_files, fn);
end
function Output(fn)
	if opts.output == nil then
		out_fn = fn;
	end
end
function Option(name)
	name = name:gsub("%-", "_");
	if opts[name] == nil then
		opts[name] = true;
		return function (value)
			opts[name] = value;
		end
	else
		return function () end;
	end
end
function GetOption(name)
	return opts[name:gsub('%-', '_')];
end
function Message(message)
	if not opts.quiet then
		print_info(message);
	end
end
function Error(message)
	if not opts.very_quiet then
		print_err(message);
	end
end
function Exit()
	os.exit(1);
end
-- -- -- -- -- -- -- --- -- -- -- -- -- -- -- --
base_path = (base_path or "."):gsub("/$", "").."/"
squishy_file = base_path .. "squishy";
out_fn = opts.output;
local ok, err = pcall(dofile, squishy_file);
if not ok then
	print_err("Couldn't read squishy file: "..err);
	os.exit(1);
end
if not out_fn then
	print_err("No output file specified by user or squishy file");
	os.exit(1);
elseif #main_files == 0 and #modules == 0 and #resources == 0 then
	print_err("No files, modules or resources. Not going to generate an empty file.");
	os.exit(1);
end
local fetch = {};
function fetch.filesystem(path)
	local f, err = io.open(path);
	if not f then return false, err; end
	local data = f:read("*a");
	f:close();
	return data;
end
if opts.use_http then
	function fetch.http(url)
		local http = require "socket.http";
		local body, status = http.request(url);
		if status == 200 then
			return body;
		end
		return false, "HTTP status code: "..tostring(status);
	end
else
	function fetch.http(url)
		return false, "Module not found. Re-squish with --use-http option to fetch it from "..url;
	end
end
print_info("Writing "..out_fn.."...");
local f, err = io.open(out_fn, "w+");
if not f then
	print_err("Couldn't open output file: "..tostring(err));
	os.exit(1);
end
if opts.executable then
	if opts.executable == true then
		f:write("#!/usr/bin/env lua\n");
	else
		f:write(opts.executable, "\n");
	end
end
if opts.debug then
	f:write(require_resource("squish.debug"));
end
print_verbose("Resolving modules...");
do
	local LUA_DIRSEP = package.config:sub(1,1);
	local LUA_PATH_MARK = package.config:sub(5,5);
	local package_path = package.path:gsub("[^;]+", function (path)
			if not path:match("^%"..LUA_DIRSEP) then
				return base_path..path;
			end
		end):gsub("/%./", "/");
	local package_cpath = package.cpath:gsub("[^;]+", function (path)
			if not path:match("^%"..LUA_DIRSEP) then
				return base_path..path;
			end
		end):gsub("/%./", "/");
	function resolve_module(name, path)
	        name = name:gsub("%.", LUA_DIRSEP);
	        for c in path:gmatch("[^;]+") do
	                c = c:gsub("%"..LUA_PATH_MARK, name);
	                print_debug("Looking for "..c)
	                if f then
				print_debug("Found!");
	                        f:close();
                        return c;
			end
		end
		return nil; -- not found
	end
	for i, module in ipairs(modules) do
		if not module.path then
			module.path = resolve_module(module.name, package_path);
			if not module.path then
				print_err("Couldn't resolve module: "..module.name);
			else
				-- Strip base_path from resolved path
				module.path = module.path:gsub("^"..base_path:gsub("%p", "%%%1"), "");
			end
		end
	end
end
print_verbose("Packing modules...");
for _, module in ipairs(modules) do
	local modulename, path = module.name, module.path;
	if module.path:sub(1,1) ~= "/" then
		path = base_path..module.path;
	end
	print_debug("Packing "..modulename.." ("..path..")...");
	local data, err = fetch.filesystem(path);
	if (not data) and module.url then
		print_debug("Fetching: ".. module.url:gsub("%?", module.path))
		data, err = fetch.http(module.url:gsub("%?", module.path));
	end
	if data then
		f:write("package.preload['", modulename, "'] = (function (...)\n");
		f:write(data);
		f:write(" end)\n");
		if opts.debug then
			f:write(string.format("package.preload[%q] = ___adjust_chunk(package.preload[%q], %q);\n\n",
		end
	else
		print_err("Couldn't pack module '"..modulename.."': "..(err or "unknown error... path to module file correct?"));
		os.exit(1);
	end
end
if #resources > 0 then
	print_verbose("Packing resources...")
	f:write("do local resources = {};\n");
	for _, resource in ipairs(resources) do
		local name, path = resource.name, resource.path;
		local res_file, err = io.open(base_path..path, "rb");
		if not res_file then
			print_err("Couldn't load resource: "..tostring(err));
			os.exit(1);
		end
		local data = res_file:read("*a");
		local maxequals = 0;
		data:gsub("(=+)", function (equals_string) maxequals = math.max(maxequals, #equals_string); end);
		f:write(("resources[%q] = %q"):format(name, data));
--[[		f:write(("resources[%q] = ["):format(name), string.rep("=", maxequals+1), "[");
		f:write(data);
		f:write("]", string.rep("=", maxequals+1), "];"); ]]
	end
	if opts.virtual_io then
		local vio = require_resource("vio");
		if not vio then
			print_err("Virtual IO requested but is not enabled in this build of squish");
		else
			-- Insert vio library
			f:write(vio, "\n")
			-- Override standard functions to use vio if opening a resource
			f:write[[local io_open, io_lines = io.open, io.lines; function io.open(fn, mode)
					if not resources[fn] then
						return io_open(fn, mode);
					else
						return vio.open(resources[fn]);
				end end
				function io.lines(fn)
					if not resources[fn] then
						return io_lines(fn);
					else
						return vio.open(resources[fn]):lines()
				end end
				local _dofile = dofile;
				function dofile(fn)
					if not resources[fn] then
						return _dofile(fn);
					else
						return assert(loadstring(resources[fn]))();
				end end
				local _loadfile = loadfile;
				function loadfile(fn)
					if not resources[fn] then
						return _loadfile(fn);
					else
				end end ]]
		end
	end
	f:write[[function require_resource(name) return resources[name] or error("resource '"..tostring(name).."' not found"); end end ]]
end
print_debug("Finalising...")
for _, fn in pairs(main_files) do
	local fin, err = io.open(base_path..fn);
	if not fin then
		print_err("Failed to open "..fn..": "..err);
		os.exit(1);
	else
		f:write((fin:read("*a"):gsub("^#.-\n", "")));
		fin:close();
	end
end
f:close();
print_info("OK!");


Skia Overview 2014

< transition =slide>

One API -- many backends
- Raster [8888, 565, A8]
- GPU [opengl]
- PDF
- XPS
- Picture
- Pipe

<transition= fade>

One Team -- many clients
- Chrome
- ChromeOS
- Clank
- Android Framework
- 3rd parties (e.g. FireFox)

<transition= rotate>

<blockstyle = code>
Optimize for CPU variety
- x86 - 32bit (SSE, SSE2, ...), 64bit
- Arm - thumb, arm, NEON, ... 64bit?
- MIPS (just starting)

<transition= zoom>

Optimize for GPU variety
- Nvidia
- Qualcom
- Imagination
- ...
- ES2 -vs- ES3 -vs- Desktop profiles

Lots of testing and measuring
- build-bots
-- unittests, micro-benchmarks, image-regressions
-- http://108.170.217.252:10117/console
- webpage archives (in progress)
-- "map-reduce" server for saerching/historgrams
-- macro-benchmarks, image-reressions
-- gpu : cpu fuzzy compares

Skia Roadmap [Fall '13]

Roadmap in a nutshell
- GPU performance
- Pictures
- Images
- Fonts
- PDF

Roadmap : GPU Performance
- Clipping changes are expensive
- Texture cache optimizations
- Better batching / reordering
- Rely more on multi-sampling
- ES3/desktop features (e.g. path-rendering)
- ... continuo ad absurdum

Roadmap : Pictures
- Playback performance
-- improve culling
-- multi-core support
- Record performance
-- improve hash/cache
-- improve measuring/bbox computation
- Feedback to clients
-- annotations
-- heat-map for time spent drawing
-- peep-hole optimizations

Roadmap : Images
- HQ filtering and mipmaps
- Unpremul support
- sRGB support (future)
- Improve cache / lazy-decoding

Roadmap : Fonts
- Color emoji
- DirectWrite on windows
-- subpixel positioning!
- new FontMgr -- extended styles

Roadmap : PDF
- Android
-- perspective, color-filters
- New Viewer project
-- print-preview and more
-- can output picture / gpu directly


slides

gShowBounds = false
gUseBlurInTransitions = false

gPath = "resources/lua/"

function load_file(file)
    local prev_path = package.path
    package.path = package.path .. ";" .. gPath .. file .. ".lua"
    require(file)
    package.path = prev_path
end

load_file("slides_utils")

gSlides = parse_file(io.open("resources/lua/slides_content2.lua", "r"))

function make_rect(l, t, r, b)
    return { left = l, top = t, right = r, bottom = b }
end

function make_paint(typefacename, style, size, color)
    local paint = Sk.newPaint();
    paint:setAntiAlias(true)
    paint:setSubpixelText(true)
    paint:setTypeface(Sk.newTypeface(typefacename, style))
    paint:setTextSize(size)
    paint:setColor(color)
    return paint
end

function draw_bullet(canvas, x, y, paint, indent)
    if 0 == indent then
        return
    end
    local ps = paint:getTextSize()
    local cx = x - ps * .8
    local cy = y - ps * .4
    local radius = ps * .2
    canvas:drawCircle(cx, cy, radius, paint)
end

function stroke_rect(canvas, rect, color)
    local paint = Sk.newPaint()
    paint:setStroke(true);
    paint:setColor(color)
    canvas:drawRect(rect, paint)
end

function drawSlide(canvas, slide, master_template)

    if #slide == 1 then
        template = master_template.title
        canvas:drawText(slide[1].text, 320, 240, template[1])
        return
    end

    template = master_template.slide

    local x = template.margin_x
    local y = template.margin_y
    local scale = 1.25

    if slide.blockstyle == "code" then
        local paint = master_template.codePaint
        local fm = paint:getFontMetrics()
        local height = #slide * (fm.descent - fm.ascent)
        y = (480 - height) / 2
        for i = 1, #slide do
            local node = slide[i]
            y = y - fm.ascent * scale
            canvas:drawText(node.text, x, y, paint)
            y = y + fm.descent * scale
        end
        return
    end

    for i = 1, #slide do
        local node = slide[i]
        local paint = template[node.indent + 1].paint
        local extra_dy = template[node.indent + 1].extra_dy
        local fm = paint:getFontMetrics()
        local x_offset = -fm.ascent * node.indent * 1.25

        local bounds = make_rect(x + x_offset, y, 620, 640)
        local blob, newBottom = Sk.newTextBlob(node.text, bounds, paint)
        draw_bullet(canvas, x + x_offset, y - fm.ascent, paint, node.indent)
        canvas:drawTextBlob(blob, 0, 0, paint)
        y = newBottom + paint:getTextSize() * .5 + extra_dy

        if gShowBounds then
            bounds.bottom = newBottom
            stroke_rect(canvas, bounds, {a=1,r=0,g=1,b=0})
            stroke_rect(canvas, blob:bounds(), {a=1,r=1,g=0,b=0})
        end

    end
end

--------------------------------------------------------------------------------------
function make_tmpl(paint, extra_dy)
    return { paint = paint, extra_dy = extra_dy }
end

function SkiaPoint_make_template()
    normal = Sk.newFontStyle()
    bold = Sk.newFontStyle(700)
    local title = {
        margin_x = 30,
        margin_y = 100,
    }
    title[1] = make_paint("Arial", bold, 45, { a=1, r=1, g=1, b=1 })
    title[1]:setTextAlign("center")
    title[2] = make_paint("Arial", bold, 25, { a=1, r=.75, g=.75, b=.75 })
    title[2]:setTextAlign("center")

    local slide = {
        margin_x = 20,
        margin_y = 25,
    }
    slide[1] = make_tmpl(make_paint("Arial", bold, 35, { a=1, r=1, g=1, b=1 }), 18)
    slide[2] = make_tmpl(make_paint("Arial", normal, 25, { a=1, r=1, g=1, b=1 }), 10)
    slide[3] = make_tmpl(make_paint("Arial", normal, 20, { a=1, r=.9, g=.9, b=.9 }), 5)

    return {
        title = title,
        slide = slide,
        codePaint = make_paint("Courier", normal, 20, { a=1, r=.9, g=.9, b=.9 }),
    }
end

gTemplate = SkiaPoint_make_template()

gRedPaint = Sk.newPaint()
gRedPaint:setAntiAlias(true)
gRedPaint:setColor{a=1, r=1, g=0, b=0 }

-- animation.proc is passed the canvas before drawing.
-- The animation.proc returns itself or another animation (which means keep animating)
-- or it returns nil, which stops the animation.
--
local gCurrAnimation

gSlideIndex = 1

-----------------------------------------------------------------------------

function new_drawable_picture(pic)
    return {
        picture = pic,
        width = pic:width(),
        height = pic:height(),
        draw = function (self, canvas, x, y, paint)
            canvas:drawPicture(self.picture, x, y, paint)
        end
    }
end

function new_drawable_image(img)
    return {
        image = img,
        width = img:width(),
        height = img:height(),
        draw = function (self, canvas, x, y, paint)
            canvas:drawImage(self.image, x, y, paint)
        end
    }
end

function convert_to_picture_drawable(slide)
    local rec = Sk.newPictureRecorder()
    drawSlide(rec:beginRecording(640, 480), slide, gTemplate)
    return new_drawable_picture(rec:endRecording())
end

function convert_to_image_drawable(slide)
    local surf = Sk.newRasterSurface(640, 480)
    drawSlide(surf:getCanvas(), slide, gTemplate)
    return new_drawable_image(surf:newImageSnapshot())
end

function new_drawable_slide(slide)
    return {
        slide = slide,
        draw = function (self, canvas, x, y, paint)
            if (nil == paint or ("number" == type(paint) and (1 == paint))) then
                canvas:save()
            else
                canvas:saveLayer(paint)
            end
            canvas:translate(x, y)
            drawSlide(canvas, self.slide, gTemplate)
            canvas:restore()
        end
    }
end

gNewDrawableFactory = {
    default = new_drawable_slide,
    picture = convert_to_picture_drawable,
    image = convert_to_image_drawable,
}

-----------------------------------------------------------------------------

function next_slide()
    local prev = gSlides[gSlideIndex]

    if gSlideIndex < #gSlides then
        gSlideIndex = gSlideIndex + 1
        spawn_transition(prev, gSlides[gSlideIndex], true)
    end
end

function prev_slide()
    local prev = gSlides[gSlideIndex]

    if gSlideIndex > 1 then
        gSlideIndex = gSlideIndex - 1
        spawn_transition(prev, gSlides[gSlideIndex], false)
    end
end

gDrawableType = "default"

load_file("slides_transitions")

function spawn_transition(prevSlide, nextSlide, is_forward)
    local transition
    if is_forward then
        transition = gTransitionTable[nextSlide.transition]
    else
        transition = gTransitionTable[prevSlide.transition]
    end

    if not transition then
        transition = fade_slide_transition
    end

    local prevDrawable = gNewDrawableFactory[gDrawableType](prevSlide)
    local nextDrawable = gNewDrawableFactory[gDrawableType](nextSlide)
    gCurrAnimation = transition(prevDrawable, nextDrawable, is_forward)
end

--------------------------------------------------------------------------------------

function spawn_rotate_animation()
    gCurrAnimation = {
        angle = 0,
        angle_delta = 5,
        pivot_x = 320,
        pivot_y = 240,
        proc = function (self, canvas, drawSlideProc)
            if self.angle >= 360 then
                drawSlideProc(canvas)
                return nil
            end
            canvas:translate(self.pivot_x, self.pivot_y)
            canvas:rotate(self.angle)
            canvas:translate(-self.pivot_x, -self.pivot_y)
            drawSlideProc(canvas)

            self.angle = self.angle + self.angle_delta
            return self
        end
    }
end

function spawn_scale_animation()
    gCurrAnimation = {
        scale = 1,
        scale_delta = .95,
        scale_limit = 0.2,
        pivot_x = 320,
        pivot_y = 240,
        proc = function (self, canvas, drawSlideProc)
            if self.scale < self.scale_limit then
                self.scale = self.scale_limit
                self.scale_delta = 1 / self.scale_delta
            end
            if self.scale > 1 then
                drawSlideProc(canvas)
                return nil
            end
            canvas:translate(self.pivot_x, self.pivot_y)
            canvas:scale(self.scale, self.scale)
            canvas:translate(-self.pivot_x, -self.pivot_y)
            drawSlideProc(canvas)

            self.scale = self.scale * self.scale_delta
            return self
        end
    }
end

local bgPaint = nil

function draw_bg(canvas)
    if not bgPaint then
        bgPaint = Sk.newPaint()
        local grad = Sk.newLinearGradient(  0,   0, { a=1, r=0, g=0, b=.3 },
                                          640, 480, { a=1, r=0, g=0, b=.8 })
        bgPaint:setShader(grad)
        bgPaint:setDither(true)
    end

    canvas:drawPaint(bgPaint)
end

function onDrawContent(canvas, width, height)
    local matrix = Sk.newMatrix()
    matrix:setRectToRect(make_rect(0, 0, 640, 480), make_rect(0, 0, width, height), "center")
    canvas:concat(matrix)

    draw_bg(canvas)

    local drawSlideProc = function(canvas)
        drawSlide(canvas, gSlides[gSlideIndex], gTemplate)
    end

    if gCurrAnimation then
        gCurrAnimation = gCurrAnimation:proc(canvas, drawSlideProc)
        return true
    else
        drawSlideProc(canvas)
        return false
    end
end

function onClickHandler(x, y)
    return false
end

local keyProcs = {
    n = next_slide,
    p = prev_slide,
    r = spawn_rotate_animation,
    s = spawn_scale_animation,
    ["="] = function () scale_text_delta(gTemplate, 1) end,
    ["-"] = function () scale_text_delta(gTemplate, -1) end,

    b = function () gShowBounds = not gShowBounds end,
    B = function () gUseBlurInTransitions = not gUseBlurInTransitions end,

    ["1"] = function () gDrawableType = "default" end,
    ["2"] = function () gDrawableType = "picture" end,
    ["3"] = function () gDrawableType = "image" end,
}

function onCharHandler(uni)
    local proc = keyProcs[uni]
    if proc then
        proc()
        return true
    end
    return false
end


function scale_text_delta(template, delta)
    template = template.slide
    for i = 1, #template do
        local paint = template[i].paint
        paint:setTextSize(paint:getTextSize() + delta)
    end
end

function slide_transition(prev, next, is_forward)
    local rec = {
        proc = function(self, canvas, drawSlideProc)
            if self:isDone() then
                drawSlideProc(canvas)
                return nil
            end
            self.prevDrawable:draw(canvas, self.curr_x, 0)
            self.nextDrawable:draw(canvas, self.curr_x + 640, 0)
            self.curr_x = self.curr_x + self.step_x
            return self
        end
    }
    if is_forward then
        rec.prevDrawable = prev
        rec.nextDrawable = next
        rec.curr_x = 0
        rec.step_x = -15
        rec.isDone = function (self) return self.curr_x <= -640 end
    else
        rec.prevDrawable = next
        rec.nextDrawable = prev
        rec.curr_x = -640
        rec.step_x = 15
        rec.isDone = function (self) return self.curr_x >= 0 end
    end
    return rec
end

function sqr(value) return value * value end

function set_blur(paint, alpha)
    local sigma = sqr(1 - alpha) * 20
    if gUseBlurInTransitions then
        paint:setImageFilter(Sk.newBlurImageFilter(sigma, sigma))
    end
    paint:setAlpha(alpha)
end

function fade_slide_transition(prev, next, is_forward)
    local rec = {
        paint = Sk.newPaint(),
        prevDrawable = prev,
        nextDrawable = next,
        proc = function(self, canvas, drawSlideProc)
            if self:isDone() then
                drawSlideProc(canvas)
                return nil
            end

            set_blur(self.paint, self.prev_a)
            self.prevDrawable:draw(canvas, self.prev_x, 0, self.paint)

            set_blur(self.paint, self.next_a)
            self.nextDrawable:draw(canvas, self.next_x, 0, self.paint)
            self:step()
            return self
        end
    }
    if is_forward then
        rec.prev_x = 0
        rec.prev_a = 1
        rec.next_x = 640
        rec.next_a = 0
        rec.isDone = function (self) return self.next_x <= 0 end
        rec.step = function (self)
            self.next_x = self.next_x - 20
            self.next_a = (640 - self.next_x) / 640
            self.prev_a = 1 - self.next_a
        end
    else
        rec.prev_x = 0
        rec.prev_a = 1
        rec.next_x = 0
        rec.next_a = 0
        rec.isDone = function (self) return self.prev_x >= 640 end
        rec.step = function (self)
            self.prev_x = self.prev_x + 20
            self.prev_a = (640 - self.prev_x) / 640
            self.next_a = 1 - self.prev_a
        end
    end
    return rec
end

function fade_transition(prev, next, is_forward)
    local rec = {
        paint = Sk.newPaint(),
        prevDrawable = prev,
        nextDrawable = next,
        proc = function(self, canvas, drawSlideProc)
            if self:isDone() then
                drawSlideProc(canvas)
                return nil
            end

            set_blur(self.paint, self.prev_a)
            self.prevDrawable:draw(canvas, 0, 0, self.paint)

            set_blur(self.paint, self.next_a)
            self.nextDrawable:draw(canvas, 0, 0, self.paint)
            self:step()
            return self
        end
    }
    rec.prev_a = 1
    rec.next_a = 0
    rec.isDone = function (self) return self.next_a >= 1 end
    rec.step = function (self)
        self.prev_a = math.max(self.prev_a - 0.025, 0)
        self.next_a = 1 - self.prev_a
    end

    return rec
end

function rotate_transition(prev, next, is_forward)
    local rec = {
        angle = 0,
        prevDrawable = prev,
        nextDrawable = next,
        activeDrawable = prev,
        proc = function(self, canvas, drawSlideProc)
            if self:isDone() then
                drawSlideProc(canvas)
                return nil
            end

            canvas:save()
            canvas:translate(320, 240)
            canvas:rotate(self.angle)
            canvas:translate(-320, -240)
            self.activeDrawable:draw(canvas, 0, 0)
            self:step()
            return self
        end,
        isDone = function (self) return self.angle >= 360 or self.angle <= -360 end
    }
    if is_forward then
        rec.step = function (self)
            self.angle = self.angle + 10
            if self.angle >= 180 then
                self.activeDrawable = self.nextDrawable
            end
        end
    else
        rec.step = function (self)
            self.angle = self.angle - 10
            if self.angle <= -180 then
                self.activeDrawable = self.nextDrawable
            end
        end
    end
    return rec
end

function zoom_transition(prev, next, is_forward)
    local rec = {
        scale = 1,
        scale_delta = .95,
        scale_limit = 0.2,
        pivot_x = 320,
        pivot_y = 240,
        prevDrawable = prev,
        nextDrawable = next,
        activeDrawable = prev,
        proc = function(self, canvas, drawSlideProc)
            if self:isDone() then
                drawSlideProc(canvas)
                return nil
            end

            canvas:translate(self.pivot_x, self.pivot_y)
            canvas:scale(self.scale, self.scale)
            canvas:translate(-self.pivot_x, -self.pivot_y)
            self.activeDrawable:draw(canvas, 0, 0)
            self:step()
            return self
        end,
        isDone = function (self) return self.scale > 1 end,
        step = function (self)
            if self.scale < self.scale_limit then
                self.scale = self.scale_limit
                self.scale_delta = 1 / self.scale_delta
                self.activeDrawable = self.nextDrawable
            end
            self.scale = self.scale * self.scale_delta
        end
    }
    return rec
end

gTransitionTable = {
    fade = fade_transition,
    slide = slide_transition,
    fade_slide = fade_slide_transition,
    rotate = rotate_transition,
    zoom = zoom_transition,
}


function tostr(t)
    local str = ""
    for k, v in next, t do
        if #str > 0 then
            str = str .. ", "
        end
        if type(k) == "number" then
            str = str .. "[" .. k .. "] = "
        else
            str = str .. tostring(k) .. " = "
        end
        if type(v) == "table" then
            str = str .. "{ " .. tostr(v) .. " }"
        elseif type(v) == "string" then
            str = str .. '"' .. v .. '"'
        else
            str = str .. tostring(v)
        end
    end
    return str
end

function trim_ws(s)
    return s:match("^%s*(.*)")
end

function count_hypens(s)
    local leftover = s:match("^-*(.*)")
    return string.len(s) - string.len(leftover)
end

function pretty_print_slide(slide)
    io.write("{\n")
    if slide.transition then
        io.write("   transition = \"", slide.transition, "\",\n")
    end
    for i = 1, #slide do
        local node = slide[i]
        for j = 0, node.indent do
            io.write("   ")
        end
        io.write("{ ")
        io.write(tostr(node))
        io.write(" },\n")
    end
    io.write("},\n")
end

function pretty_print_slides(slides)
    io.write("gSlides = {\n")
    for i = 1, #slides do
        pretty_print_slide(slides[i])
    end
    io.write("}\n")
end

function parse_attr(s, lvalue)
    local ts = "^<%s*" .. lvalue .. "%s*=%s*(%a+)%s*>$"
    return s:match(ts)
end

function flush(slides, block)
    if #block > 0 then
        slides[#slides + 1] = block
        return {}
    end
    return block
end

function parse_file(file)
    local slides = {}
    local block = {}

    for line in file:lines() do
        local s = trim_ws(line)
        if #s == 0 then   -- done with a block
            block = flush(slides, block)
        else
            local transition_type = parse_attr(s, "transition")
            local blockstyle = parse_attr(s, "blockstyle")
            if transition_type then
                block["transition"] = transition_type
            elseif blockstyle then
                block["blockstyle"] = blockstyle
            else
                if block.blockstyle == "code" then
                    block[#block + 1] = { text = line }
                else
                    local n = count_hypens(s)
                    block[#block + 1] = {
                        indent = n,
                        text = trim_ws(s:sub(n + 1, -1))
                    }
                end
            end
        end
    end
    flush(slides, block)

    return slides
end


Skia Update

Skia : Access
- https://skia.org
- https://skia.googlesource.com/skia

Skia : Overview
- portable graphics engine
- 2D transformations + perspective
- primitives: text, geometry, images
- effects: shaders, filters, antialiasing, blending

Skia : Porting
- C++ and some SIMD assembly
- Fonts : CoreText, FreeType, GDI, DirectWrite
- Threads : wrappers for native apis
- Memory : wrappers for [new, malloc, discardable]

Skia : Backends
- Surface
-- raster : ARGB, RGB16, A8 in software
-- gpu : transcribe to OpenGL
- Document
-- transcribe to PDF or XPS
- Record and Playback
-- Picture
-- Pipe

Skia : Clients
- Blink : under the GraphicsContext hood
- Chrome : ui/gfx and compositor
- Android : framework
- third parties : e.g. Mozilla

Skia In Blink

Skia In Blink : Fonts
- SkTypeface and SkFontMgr : platform agnostic
- Runtime switch between GDI and DirectWrite
- SkTextBlob to encapsulate runs of text
- Push LCD decision-making out of Blink

Skia In Blink : Record-Time-Rasterization
- What? : direct rendering during “Paint” pass
-- Image scaling, filters
-- SVG patterns, masks
- Problematic in modern Blink
-- CTM not always known/knowable
-- Rendering backend not always known (gpu or cpu)
-- Rasterization takes (too much) time

Skia In Blink : RTR response
- SkImageFilter w/ CPU and GPU implementations
- Bitmap scaling : bilerp, mipmaps, fancy
- SkPicture for caching SVG
- SkPicture + saveLayer() for masks
-- PathOps for resolving complex paths
- SkPictureShader for device-independent patterns

Skia In Blink : Recording
- GraphicsContext (now) backed by SkPicture
-- draw commands are recorded for later playback
-- all parameters must be copied or (safely) ref'd
-- may record more than is currently visible
- Resulting picture may be replayed multiple times
-- from different thread(s)

Skia In Blink : Recording response
- New implementation
- Optimized for recording speed
-- shallow copies whenever possible
-- rearchitect all Skia effects to be immutable
- Reentrant-safe for playback in multiple threads
-- also affected effect subclasses

Skia In Blink : Playback
- Separate pass for optimizations (optional)
-- peep-holes rewrites
-- compute bounding-box hierarchy for faster tiling
-- can be done outside of Blink thread
- GPU optimizations
-- layer "hoisting"
-- distance fields : fonts and concave paths

Skia In Blink : multi-picture-draw
- mpd(canvas[], picture[], matrix[], paint[])
- Requires independent canvas objects
-- all other parameters can be shared
-- draw order is unspecified
- Examples
-- 1 picture drawing to multiple tiles (canvases)
-- multiple pictures each drawing to its own layer

Skia In Blink : MPD optimizations*
- GPU
-- "layer hoisting" to reduce rendertarget switching
-- layer atlasing (also applies to imagefilters)
-- pre-uploading of textures
-- atlas yuv (from jpeg) to convert on gpu
- CPU
-- parallel execution using thread pool
-- pre-decoding of images based on visibility

Skia : Roadmap

Skia : Roadmap - performance
- GPU
-- extended OpenGL features (e.g. geometry shaders)
-- reordering for increased batching
-- support for new low-level OpenGL APIs
- CPU
-- SIMD applied to floats
-- smarter culling in pictures

Skia : Roadmap - API
- Cross process support
- Direct support for sRGB
- Robust file format
- Support PDF viewing
- Stable C ABI
-- bindings for JS, Go, Python, Lua

Demo


test

local r = { left = 10, top = 10, right = 100, bottom = 80 }
local x = 0;

local paint = Sk.newPaint();
paint:setAntiAlias(true);

local image     -- = Sk.loadImage('/skia/sailboat.jpg');
function setImageFilename(filename)
    image = Sk.loadImage(filename)
end

local color = {a = 1, r = 1, g = 0, b = 0};

function rnd(range)
   return math.random() * range;
end

rndX = function () return rnd(640) end
rndY = function () return rnd(480) end

function draw_rand_path(canvas);
   if not path_paint then
       path_paint = Sk.newPaint();
       path_paint:setAntiAlias(true);
   end
   path_paint:setColor({a = 1, r = math.random(), g = math.random(), b = math.random() });

   local path = Sk.newPath();
   path:moveTo(rndX(), rndY());
   for i = 0, 50 do
       path:quadTo(rndX(), rndY(), rndX(), rndY());
   end
   canvas:drawPath(path, path_paint);

   paint:setColor{a=1,r=0,g=0,b=1};
   local align = { 'left', 'center', 'right' };
   paint:setTextSize(30);
   for k, v in next, align do
       paint:setTextAlign(v);
       canvas:drawText('Hamburgefons', 320, 200 + 30*k, paint);
   end
end

function onStartup()
    local paint = Sk.newPaint();
    paint:setColor{a=1, r=1, g=0, b=0};
    if false then
        local doc = Sk.newDocumentPDF('out/test.pdf');
        local canvas = doc:beginPage(72*8.5, 72*11);
        canvas:drawText('Hello Lua', 300, 300, paint);
        doc:close();
        doc = nil;
    end
end

function onDrawContent(canvas)
    draw_rand_path(canvas);
    color.g = x / 100;
    paint:setColor(color)
    canvas:translate(x, 0);
    canvas:drawOval(r, paint)
    x = x + 1;
    local r2 = {}
    r2.left = x;
    r2.top = r.bottom + 50;
    r2.right = r2.left + image:width() * 1;
    r2.bottom = r2.top + image:height() * 1;
    canvas:drawImageRect(image, nil, r2, 0.75);
    if x > 200 then x = 0 end;

    return true -- so we can animate
end

onStartup()


--
--
-- Dashed path scraping script.
-- This script is designed to count the total number of dashes in a dashed path
-- by computing the fill path and then counting how many individual segments are
-- inside the resulting fill path.
dashes = 0
pathPieces = {}
function sk_scrape_startcanvas(c, fileName)
end
function sk_scrape_endcanvas(c, fileName)
end
function sk_scrape_accumulate(t)
    local paint = t.paint
    if paint then
        local pe = paint:getPathEffect()
        if pe then
            if t.verb == "drawPath" and pe:asADash() then
                dashes = dashes + 1
                pathPieces[dashes] = 0
                local path = t.path
                local fillpath = paint:getFillPath(path)
                local verbs = fillpath:getVerbs()
                for _, verb in ipairs(verbs) do
                    if verb == "move" then
                       pathPieces[dashes] = pathPieces[dashes] + 1
                    end
                end
            end
        end
    end
end
-- We mulitply by two because for each segment of the dash, we do two measurements:
-- One for the beginning and one for the end of each dash.
function sk_scrape_summarize()
    local pieces5 = 0;
    local pieces10 = 0;
    for _, p in ipairs(pathPieces) do
        local pieces = 2*p
        if pieces < 5 then
            pieces5 = pieces5 + 1
        end
        if pieces > 5 and pieces < 10 then
            pieces10 = pieces10 + 1
        end
    end
    io.write(string.format("%d %d %d\n", 2*dashes, pieces5, pieces10))
end


function sk_scrape_startcanvas(c, fileName) end

function sk_scrape_endcanvas(c, fileName) end

function classify_rrect(rrect)
    if (rrect:type() == "simple") then
        local x, y = rrect:radii(0)
        if (x == y) then
            return "simple_circle"
        else
            return "simple_oval"
        end
    elseif (rrect:type() == "complex") then
        local numNotSquare = 0
        local rx, ry
        local same = true;
        local first_not_square_corner
        local last_not_square_corner
        for i = 1, 4 do
            local x, y = rrect:radii(i-1)
            if (x ~= 0 and y ~= 0) then
                if (numNotSquare == 0) then
                    rx = x
                    ry = y
                    first_not_square_corner = i
                else
                   last_not_square_corner = i
                   if (rx ~= x or ry ~=y) then
                       same = false
                   end
                end
                numNotSquare = numNotSquare + 1
            end
        end
        local numSquare = 4 - numNotSquare
        if (numSquare > 0 and same) then
            local corners = "corners"
            if (numSquare == 2) then
                if ((last_not_square_corner - 1 == first_not_square_corner) or
                    (1 == first_not_square_corner and 4 == last_not_square_corner )) then
                    corners = "adjacent_" .. corners
                else
                    corners = "opposite_" .. corners
                end
            elseif (1 == numSquare) then
                corners = "corner"
            end
            if (rx == ry) then
                return "circles_with_" .. numSquare .. "_square_" .. corners
            else
                return "ovals_with_" .. numSquare .. "_square_" .. corners
            end
        end
        return "complex_unclassified"
    elseif (rrect:type() == "rect") then
        return "rect"
    elseif (rrect:type() == "oval") then
        local x, y = rrect:radii(0)
        if (x == y) then
            return "circle"
        else
            return "oval"
        end
    elseif (rrect:type() == "empty") then
        return "empty"
    else
        return "unknown"
    end
end

function print_classes(class_table)
  function sort_classes(a, b)
     return a.count > b.count
  end
  array = {}
  for k, v in pairs(class_table) do
      if (type(v) == "number") then
          array[#array + 1] = {class = k, count = v};
      end
  end
  table.sort(array, sort_classes)
  local i
  for i = 1, #array do
      io.write(array[i].class, ": ", array[i].count, " (", array[i].count/class_table["total"] * 100, "%)\n");
  end
end

function sk_scrape_accumulate(t)
    if (t.verb == "clipRRect") then
        local rrect = t.rrect
        table["total"] = (table["total"] or 0) + 1
        local class = classify_rrect(rrect)
        table[class] = (table[class] or 0) + 1
    end
end

function sk_scrape_summarize()
  print_classes(table)
  --[[ To use the web scraper comment out the above call to print_classes, run the code below,
       and in the aggregator pass agg_table to print_classes.
  for k, v in pairs(table) do
      if (type(v) == "number") then
          local t = "agg_table[\"" .. k .. "\"]"
          io.write(t, " = (", t, " or 0) + ", table[k], "\n" );
      end
  end
  --]]
end


--[[
    This file is used as the aggregator file when using telemetry for
    scrape_dashing_full.lua
]]

dashCount = 0
dashTable = {}

function increment_inner(table, key, value)
    table[key] = (table[key] or 0) + value
end

function increment(table, tableKey, key, value)
    if (table[tableKey] == nil) then
        table[tableKey] = {}
    end
    increment_inner(table[tableKey], key, value)
end

dofile("/tmp/lua-output")

io.write("Total dashed effects is: ", dashCount, "\n")
for k1, v1 in next, dashTable do
    io.write("\nTable: ", k1, "\n")
    for k, v in next, v1 do
        io.write("\"", k, "\": ", v, "\n")
    end
end


-- Aggregate the output from ngrams.lua.

-- Get the data from all shards.
counts = {}
dofile("/tmp/lua-output")

-- Put the data into a sortable "array".
countArray = {}
for ngram, count in pairs(counts) do
  table.insert(countArray, {count, ngram})
end

-- Sort the data.
function compare(a, b)
  return a[1] > b[1]
end
table.sort(countArray, compare)

-- Write the result.
for i, countPair in ipairs(countArray) do
  io.write(countPair[1], "\t", countPair[2], "\n")
end


ngrams

-- Generate n-grams of Skia API calls from SKPs.

-- To test this locally, run:
-- $ GYP_DEFINES="skia_shared_lib=1" make lua_pictures
-- $ out/Debug/lua_pictures -q -r $SKP_DIR -l tools/lua/ngrams.lua > /tmp/lua-output
-- $ lua tools/lua/ngrams_aggregate.lua

-- To run on Cluster Telemetry, copy and paste the contents of this file into
-- the box at https://skia-tree-status.appspot.com/skia-telemetry/lua_script,
-- and paste the contents of ngrams_aggregate.lua into the "aggregator script"
-- box on the same page.

-- Change n as desired.
-- CHANGEME
local n = 3
-- CHANGEME

-- This algorithm uses a list-of-lists for each SKP. For API call, append a
-- list containing just the verb to the master list. Then, backtrack over the
-- last (n-1) sublists in the master list and append the verb to those
-- sublists. At the end of execution, the master list contains a sublist for
-- every verb in the SKP file. Each sublist has length n, with the exception of
-- the last n-1 sublists, which are discarded in the summarize() function,
-- which generates counts for each n-gram.

local ngrams = {}
local currentFile = ""

function sk_scrape_startcanvas(c, fileName)
  currentFile = fileName
  ngrams[currentFile] = {}
end

function sk_scrape_endcanvas(c, fileName)
end

function sk_scrape_accumulate(t)
  table.insert(ngrams[currentFile], {t.verb})
  for i = 1, n-1 do
    local idx = #ngrams[currentFile] - i
    if idx > 0 then
      table.insert(ngrams[currentFile][idx], t.verb)
    end
  end
end

function sk_scrape_summarize()
  -- Count the n-grams.
  local counts = {}
  for file, ngramsInFile in pairs(ngrams) do
    for i = 1, #ngramsInFile - (n-1) do
      local ngram = table.concat(ngramsInFile[i], " ")
      if counts[ngram] == nil then
        counts[ngram] = 1
      else
        counts[ngram] = counts[ngram] + 1
      end
    end
  end

  -- Write out code for aggregating.
  for ngram, count in pairs(counts) do
    io.write("if counts['", ngram, "'] == nil then counts['", ngram, "'] = ", count, " else counts['", ngram, "'] = counts['", ngram, "'] + ", count, " end\n")
  end
end


gradients

filename = ""

function sk_scrape_startcanvas(c, fileName)
    filename = fileName
end

function sk_scrape_endcanvas(c, fileName)

end

LuaDoubleNearlyZero = 1.0 / bit32.lshift(1.0, 12)

function LuaDoubleNearlyEqual(a, b)
    return math.abs(a-b) <= LuaDoubleNearlyZero
end

function bounds(rect)
    local width  = rect.right  - rect.left
    local height = rect.bottom - rect.top

    return width, height
end

gradients = {}

i = 1

function sk_scrape_accumulate(t)
    local p = t.paint
    if p then
        local s = p:getShader()
        if s then
            local g = s:asAGradient()
            if g then
                gradients[i] = {}

                gradients[i].filename = filename

                local width, height = -1, -1
                if t.rect then
                    width, height = bounds(t.rect)
                elseif t.rrect then
                    width, height = bounds(t.rrect:rect())
                elseif t.path then
                    width, height = bounds(t.path:getBounds())
                end
                gradients[i].boundsWidth  = width
                gradients[i].boundsHeight = height

                gradients[i].colorCount = g.colorCount
                gradients[i].type       = g.type
                gradients[i].tile       = g.tile

                isEvenlySpaced = true
                for j = 1, g.colorCount, 1 do
                    if not LuaDoubleNearlyEqual(g.positions[j], (j-1)/(g.colorCount-1)) then
                        isEvenlySpaced = false
                    end
                end
                gradients[i].isEvenlySpaced = isEvenlySpaced

                numHardStops = 0
                for j = 2, g.colorCount, 1 do
                    if LuaDoubleNearlyEqual(g.positions[j], g.positions[j-1]) then
                        numHardStops = numHardStops + 1
                    end
                end
                gradients[i].numHardStops = numHardStops

                gradients[i].verb = t.verb

                gradients[i].positions = {}
                for j = 1, g.colorCount, 1 do
                    gradients[i].positions[j] = g.positions[j]
                end

                i = i + 1
            end
        end
    end
end

function sk_scrape_summarize()
    for k, v in pairs(gradients) do
        local pos = ""
        for j = 1, v.colorCount , 1 do
            pos = pos .. v.positions[j]
            if j ~= v.colorCount then
                pos = pos .. ","
            end
        end

        io.write(string.format("%s %d %s %s %d %d %s %d %d %s\n",
                                v.filename,
                                v.colorCount,
                                v.type,
                                v.tile,
                                tonumber(v.isEvenlySpaced and 1 or 0),
                                v.numHardStops,
                                v.verb,
                                v.boundsWidth,
                                v.boundsHeight,
                                pos))
    end
end


local canvas        -- holds the current canvas (from startcanvas())

--[[
    startcanvas() is called at the start of each picture file, passing the
    canvas that we will be drawing into, and the name of the file.

    Following this call, there will be some number of calls to accumulate(t)
    where t is a table of parameters that were passed to that draw-op.

        t.verb is a string holding the name of the draw-op (e.g. "drawRect")

    when a given picture is done, we call endcanvas(canvas, fileName)
]]
function sk_scrape_startcanvas(c, fileName)
    canvas = c
end

--[[
    Called when the current canvas is done drawing.
]]
function sk_scrape_endcanvas(c, fileName)
    canvas = nil
end

--[[
    Use to initialize all keys passed in keyTable to zero in table.
    Useful so that keys that are never get incremented still output zero at end
]]
function resetTableKeys(table, keyTable)
    for k, v in next, keyTable do
        table[v] = 0
    end
end

function increment(table, key)
    table[key] = (table[key] or 0) + 1
end

local dashCount = 0

local total_found = {}
local drawPoints_count = {}
local drawPoints_direction = {}
resetTableKeys(drawPoints_direction, {"hori", "vert", "other"})
local dashInterval_count = {}
local dashInterval_pattern = {}
resetTableKeys(dashInterval_pattern, {"one_one", "zero_on", "other"})
local dash_phase = {}
resetTableKeys(dash_phase, {"zero", "other"})
local dash_cap = {}
resetTableKeys(dash_cap, {"butt", "round", "square"})

local dashTable = {}
dashTable.total_found = total_found
dashTable.drawPoints_count = drawPoints_count
dashTable.drawPoints_direction = drawPoints_direction
dashTable.dashInterval_count = dashInterval_count
dashTable.dashInterval_pattern = dashInterval_pattern
dashTable.dash_phase = dash_phase
dashTable.dash_cap = dash_cap

function sk_scrape_accumulate(t)
    local p = t.paint
    if p then
        local pe = p:getPathEffect()
        if pe then
            local de = pe:asADash()
            if de then
                dashCount = dashCount + 1
                increment(total_found, t.verb);
                increment(dashInterval_count, #de.intervals)
                if 2 == #de.intervals then
		    if 1 == de.intervals[1] and 1 == de.intervals[2] then
                        increment(dashInterval_pattern, "one_one")
                    elseif 0 == de.intervals[1] then
                        increment(dashInterval_pattern, "zero_on")
                    else
                        increment(dashInterval_pattern, "other")
                    end
                end

                if 0 == de.phase then
                    increment(dash_phase, "zero")
                else
                    increment(dash_phase, "other")
                end

                local cap = p:getStrokeCap()
                if 0 == cap then
                    increment(dash_cap, "butt")
                elseif 1 == cap then
                    increment(dash_cap, "round")
                else
                    increment(dash_cap, "square")
                end

                if "drawPoints" == t.verb then
                    local points = t.points
                    increment(drawPoints_count, #points)
                    if 2 == #points then
                        if points[1].y == points[2].y then
                            increment(drawPoints_direction, "hori")
                        elseif points[1].x == points[2].x then
                            increment(drawPoints_direction, "vert")
                        else
                            increment(drawPoints_direction, "other")
                        end
                    end
                end

                --[[
                    eventually would like to print out info on drawPath verbs with dashed effect
                ]]
                if "drawPath" == t.verb then
                end

            end
        end
    end
end

--[[
    lua_pictures will call this function after all of the pictures have been
    "accumulated".
]]
function sk_scrape_summarize()
-- use for non telemetry
--[[
    io.write("Total dashed effects is: ", dashCount, "\n");
    for k1, v1 in next, dashTable do
        io.write("\nTable: ", k1, "\n")
        for k, v in next, v1 do
            io.write("\"", k, "\": ", v, "\n")
        end
    end
]]

-- use for telemetry
    io.write("\ndashCount = dashCount + ", tostring(dashCount), "\n")
    for k1, v1 in next, dashTable do
        for k, v in next, v1 do
            io.write("\nincrement(dashTable, \"", k1, "\", \"", k, "\", ", v, ")\n")
        end
    end
end


chars-vs-glyphs

local canvas

function sk_scrape_startcanvas(c, fileName)
    canvas = c
end

function sk_scrape_endcanvas(c, fileName)
    canvas = nil
end

local glyph_calls = 0
local unichar_calls = 0

local isTextVerbs = {
    drawPosText = true,
    drawPosTextH = true,
    drawText = true,
    drawTextOnPath = true,
}

function sk_scrape_accumulate(t)
    if isTextVerbs[t.verb] then
        if t.glyphs then
            glyph_calls = glyph_calls + 1
        else
            unichar_calls = unichar_calls + 1
        end
    end
end

function sk_scrape_summarize()
    io.write("glyph calls = ", glyph_calls,
             ", unichar calls = ", unichar_calls, "\n");
end


paths

--
--
-- Path scraping script.
-- This script is designed to count the number of times we fall back to software
-- rendering for a path in a given SKP. However, this script does not count an exact
-- number of uploads, since there is some overlap with clipping: e.g. two clipped paths
-- may cause three uploads to the GPU (set clip 1, set clip 2, unset clip 2/reset clip 1),
-- but these cases are rare.
draws = 0
drawPaths = 0
drawPathsAnti = 0
drawPathsConvexAnti = 0
clips = 0
clipPaths = 0
clipPathsAnti = 0
clipPathsConvexAnti = 0
usedPath = false
usedSWPath = false
skpsTotal = 0
skpsWithPath = 0
skpsWithSWPath = 0
function sk_scrape_startcanvas(c, fileName)
   usedPath = false
   usedSWPath = false
end
function sk_scrape_endcanvas(c, fileName)
   skpsTotal = skpsTotal + 1
   if usedPath then
      skpsWithPath = skpsWithPath + 1
      if usedSWPath then
         skpsWithSWPath = skpsWithSWPath + 1
      end
   end
end
function string.starts(String,Start)
   return string.sub(String,1,string.len(Start))==Start
end
function isPathValid(path)
   if not path then
      return false
   end
   if path:isEmpty() then
      return false
   end
   if path:isRect() then
      return false
   end
   return true
end
function sk_scrape_accumulate(t)
   if (string.starts(t.verb, "draw")) then
      draws = draws + 1
   end
   if (string.starts(t.verb, "clip")) then
      clips = clips + 1
   end
   if t.verb == "clipPath" then
      local path = t.path
      if isPathValid(path) then
         clipPaths = clipPaths + 1
         usedPath = true
         if t.aa then
            clipPathsAnti = clipPathsAnti + 1
            if path:isConvex() then
               clipPathsConvexAnti = clipPathsConvexAnti + 1
            else
               usedSWPath = true
            end
         end
      end
   end
   if t.verb == "drawPath" then
      local path = t.path
      local paint = t.paint
      if paint and isPathValid(path) then
         drawPaths = drawPaths + 1
         usedPath = true
         if paint:isAntiAlias() then
            drawPathsAnti = drawPathsAnti + 1
            if path:isConvex() then
               drawPathsConvexAnti = drawPathsConvexAnti + 1
            else
               usedSWPath = true
            end
         end
      end
   end
end
function sk_scrape_summarize()
   local swDrawPaths = drawPathsAnti - drawPathsConvexAnti
   local swClipPaths = clipPathsAnti - clipPathsConvexAnti
   io.write("clips = clips + ", clips, "\n");
   io.write("draws = draws + ", draws, "\n");
   io.write("clipPaths = clipPaths + ", clipPaths, "\n");
   io.write("drawPaths = drawPaths + ", drawPaths, "\n");
   io.write("swClipPaths = swClipPaths + ", swClipPaths, "\n");
   io.write("swDrawPaths = swDrawPaths + ", swDrawPaths, "\n");
   io.write("skpsTotal = skpsTotal + ", skpsTotal, "\n");
   io.write("skpsWithPath = skpsWithPath + ", skpsWithPath, "\n");
   io.write("skpsWithSWPath = skpsWithSWPath + ", skpsWithSWPath, "\n");
end


xfer-counter

function sk_scrape_startcanvas(c, fileName)
end

function sk_scrape_endcanvas(c, fileName)
end

local gXM_Count = 0
local gXferModeTab = {}

function sk_scrape_accumulate(t)
    if not t.paint then
        return
    end

    local xferMode = t.paint:getXfermode()

    if xferMode then
        local modeName = xferMode:getTypeName()

        if gXferModeTab[modeName] == nil then
            gXferModeTab[modeName] = 1;
        else
            gXferModeTab[modeName] = gXferModeTab[modeName] + 1
        end
        gXM_Count = gXM_Count + 1
    end
end

function sk_scrape_summarize()
    for key,value in pairs(gXferModeTab) do
        io.write(key, ": ", value, "\n")
    end
    io.write("total: ", gXM_Count)
end

function test_summary()
    io.write("just testing test_summary\n")
end


function string.startsWith(String,Start)
   return string.sub(String,1,string.len(Start))==Start
end

function string.endsWith(String,End)
   return End=='' or string.sub(String,-string.len(End))==End
end

local canvas = nil
local num_perspective_bitmaps = 0
local num_affine_bitmaps = 0
local num_scaled_bitmaps = 0
local num_translated_bitmaps = 0
local num_identity_bitmaps = 0
local num_scaled_up = 0
local num_scaled_down = 0

function sk_scrape_startcanvas(c, fileName)
  canvas = c
end

function sk_scrape_endcanvas(c, fileName)
  canvas = nil
end

function sk_scrape_accumulate(t)
    -- dump the params in t, specifically showing the verb first, which we
    -- then nil out so it doesn't appear in tostr()
    if (string.startsWith(t.verb,"drawBitmap")) then
      matrix = canvas:getTotalMatrix()
      matrixType = matrix:getType()
      if matrixType.perspective then
        num_perspective_bitmaps = num_perspective_bitmaps + 1
      elseif matrixType.affine then
        num_affine_bitmaps = num_affine_bitmaps + 1
      elseif matrixType.scale then
        num_scaled_bitmaps = num_scaled_bitmaps + 1
        if matrix:getScaleX() > 1 or matrix:getScaleY() > 1 then
          num_scaled_up = num_scaled_up + 1
        else
          num_scaled_down = num_scaled_down + 1
        end
      elseif matrixType.translate then
        num_translated_bitmaps = num_translated_bitmaps + 1
      else
        num_identity_bitmaps = num_identity_bitmaps + 1
      end
    end
end

function sk_scrape_summarize()
  io.write( "identity = ", num_identity_bitmaps,
            ", translated = ", num_translated_bitmaps,
            ", scaled = ", num_scaled_bitmaps, " (up = ", num_scaled_up, "; down = ", num_scaled_down, ")",
            ", affine = ", num_affine_bitmaps,
            ", perspective = ", num_perspective_bitmaps,
            "\n")
end


dumpops

function tostr(t)
    local str = ""
    for k, v in next, t do
        if #str > 0 then
            str = str .. ", "
        end
        if type(k) == "number" then
            str = str .. "[" .. k .. "] = "
        else
            str = str .. tostring(k) .. " = "
        end
        if type(v) == "table" then
            str = str .. "{ " .. tostr(v) .. " }"
        else
            str = str .. tostring(v)
        end
    end
    return str
end

function sk_scrape_startcanvas(c, fileName) end

function sk_scrape_endcanvas(c, fileName) end

function sk_scrape_accumulate(t)
    -- dump the params in t, specifically showing the verb first, which we
    -- then nil out so it doesn't appear in tostr()
    io.write(t.verb, " ")
    t.verb = nil
    io.write(tostr(t), "\n")
end

function sk_scrape_summarize() end


skia

-- Experimental helpers for skia --

function string.startsWith(String,Start)
   return string.sub(String,1,string.len(Start))==Start
end

function string.endsWith(String,End)
   return End=='' or string.sub(String,-string.len(End))==End
end

Sk = {}

function Sk.isFinite(x)
    return x * 0 == 0
end

-------------------------------------------------------------------------------

Sk.Rect = { left = 0, top = 0, right = 0, bottom = 0 }
Sk.Rect.__index = Sk.Rect

function Sk.Rect.new(l, t, r, b)
    local rect
    if r then
        -- 4 arguments
        rect = { left = l, top = t, right = r, bottom = b }
    elseif l then
        -- 2 arguments
        rect = { right = l, bottom = t }
    else
        -- 0 arguments
        rect = {}
    end
    setmetatable(rect, Sk.Rect)
    return rect;
end

function Sk.Rect:width()
    return self.right - self.left
end

function Sk.Rect:height()
    return self.bottom - self.top
end

function Sk.Rect:isEmpty()
    return self:width() <= 0 or self:height() <= 0
end

function Sk.Rect:isFinite()
    local value = self.left * 0
    value = value * self.top
    value = value * self.right
    value = value * self.bottom
    return 0 == value
end

function Sk.Rect:setEmpty()
    self.left = 0
    self.top = 0
    self.right = 0
    self.bottom = 0
end

function Sk.Rect:set(l, t, r, b)
    self.left = l
    self.top = t
    self.right = r
    self.bottom = b
end

function Sk.Rect:offset(dx, dy)
    dy = dy or dx

    self.left = self.left + dx
    self.top = self.top + dy
    self.right = self.right + dx
    self.bottom = self.bottom + dy
end

function Sk.Rect:inset(dx, dy)
    dy = dy or dx

    self.left = self.left + dx
    self.top = self.top + dy
    self.right = self.right - dx
    self.bottom = self.bottom - dy
end

-------------------------------------------------------------------------------


-- bbh_filter.lua
--
-- This script outputs info about 'interesting' skp files,
-- where the definition of 'interesting' changes but is roughly:
-- "Interesting for bounding box hierarchy benchmarks."
--
-- Currently, the approach is to output, in equal ammounts, the names of the files that
-- have most commands, and the names of the files that use the least popular commands.

function count_entries(table)
    local count = 0
    for _,_ in pairs(table) do
        count = count + 1
    end
    return count
end

verbCounts = {}

function reset_current()
    -- Data about the skp in transit
    currentInfo = {
        fileName = '',
        verbs = {},
        numOps = 0
    }
end
reset_current()

numOutputFiles = 10  -- This is per measure.
globalInfo = {}  -- Saves currentInfo for each file to be used at the end.
output = {}  -- Stores {fileName, {verb, count}} tables.

function tostr(t)
    local str = ""
    for k, v in next, t do
        if #str > 0 then
            str = str .. ", "
        end
        if type(k) == "number" then
            str = str .. "[" .. k .. "] = "
        else
            str = str .. tostring(k) .. " = "
        end
        if type(v) == "table" then
            str = str .. "{ " .. tostr(v) .. " }"
        else
            str = str .. tostring(v)
        end
    end
    return str
end

function sk_scrape_startcanvas(c, fileName) end

function sk_scrape_endcanvas(c, fileName)
    globalInfo[fileName] = currentInfo
    globalInfo[fileName].fileName = fileName
    reset_current()
end

function sk_scrape_accumulate(t)
    -- dump the params in t, specifically showing the verb first, which we
    -- then nil out so it doesn't appear in tostr()
    --
    verbCounts[t.verb] = (verbCounts[t.verb] or 0) + 1
    currentInfo.verbs[t.verb] = (currentInfo.verbs[t.verb] or 0) + 1
    currentInfo.numOps = currentInfo.numOps + 1

    t.verb = nil
end

function sk_scrape_summarize()
    verbWeights = {}  -- {verb, weight}, where 0 < weight <= 1

    meta = {}
    for k,v in pairs(verbCounts) do
        table.insert(meta, {key=k, value=v})
    end
    table.sort(meta, function (a,b) return a.value > b.value; end)
    maxValue = meta[1].value
    io.write("-- ==================\n")
    io.write("------------------------------------------------------------------ \n")
    io.write("-- Command\t\t\tNumber of calls\t\tPopularity\n")
    io.write("------------------------------------------------------------------ \n")
    for k, v in pairs(meta) do
        verbWeights[v.key] = v.value / maxValue

        -- Poor man's formatting:
        local padding = "\t\t\t"
        if (#v.key + 3) < 8 then
            padding = "\t\t\t\t"
        end
        if (#v.key + 3) >= 16 then
            padding = "\t\t"
        end

        io.write ("-- ",v.key, padding, v.value, '\t\t\t', verbWeights[v.key], "\n")
    end

    meta = {}
    function calculate_weight(verbs)
        local weight = 0
        for name, count in pairs(verbs) do
            weight = weight + (1 / verbWeights[name]) * count
        end
        return weight
    end
    for n, info in pairs(globalInfo) do
        table.insert(meta, info)
    end

    local visitedFiles = {}

    -- Prints out information in lua readable format
    function output_with_metric(metric_func, description, numOutputFiles)
        table.sort(meta, metric_func)
        print(description)
        local iter = 0
        for i, t in pairs(meta) do
            if not visitedFiles[t.fileName] then
                visitedFiles[t.fileName] = true
                io.write ("{\nname = \"", t.fileName, "\", \nverbs = {\n")
                for verb,count in pairs(globalInfo[t.fileName].verbs) do
                    io.write('    ', verb, " = ", count, ",\n")
                end
                io.write("}\n},\n")

                iter = iter + 1
                if iter >= numOutputFiles then
                    break
                end
            end
        end
    end

    output_with_metric(
        function(a, b) return calculate_weight(a.verbs) > calculate_weight(b.verbs); end,
        "\n-- ================== skps with calling unpopular commands.", 10)
    output_with_metric(
        function(a, b) return a.numOps > b.numOps; end,
        "\n-- ================== skps with the most calls.", 50)

    local count = count_entries(visitedFiles)

    print ("-- Spat", count, "files")
end


glyph-counts

function tostr(t)
    local str = ""
    for k, v in next, t do
        if #str > 0 then
            str = str .. ", "
        end
        if type(k) == "number" then
            str = str .. "[" .. k .. "] = "
        else
            str = str .. tostring(k) .. " = "
        end
        if type(v) == "table" then
            str = str .. "{ " .. tostr(v) .. " }"
        else
            str = str .. tostring(v)
        end
    end
    return str
end

local canvas        -- holds the current canvas (from startcanvas())

--[[
    startcanvas() is called at the start of each picture file, passing the
    canvas that we will be drawing into, and the name of the file.

    Following this call, there will be some number of calls to accumulate(t)
    where t is a table of parameters that were passed to that draw-op.

        t.verb is a string holding the name of the draw-op (e.g. "drawRect")

    when a given picture is done, we call endcanvas(canvas, fileName)
]]
function sk_scrape_startcanvas(c, fileName)
    canvas = c
end

--[[
    Called when the current canvas is done drawing.
]]
function sk_scrape_endcanvas(c, fileName)
    canvas = nil
end

--[[
    Called with the parameters to each canvas.draw call, where canvas is the
    current canvas as set by startcanvas()
]]

local gCounts = {}  -- [fontID_pointsize] = [] unique glyphs
local gFirstGlyphs = {}
local gTotalCount = 0

function array_count(array)
    local n = 0
    for k in next, array do
        n = n + 1
    end
    return n
end

function sk_scrape_accumulate(t)
    verb = t.verb;
    if verb == "drawPosText" or verb == "drawPosTextH" then
        if t.glyphs then
            local key = array_count(t.glyphs)
            local n = gCounts[key]
            if n then
                gCounts[key] = n + 1
            else
                gCounts[key] = 1
            end

            if key == 1 then
                local first = t.glyphs[1];
                local n = gFirstGlyphs[first]
                if n then
                    n = n + 1
                else
                    n = 0
                end
                gFirstGlyphs[first] = n
            end

            gTotalCount = gTotalCount + 1
        end
    end
end

--[[
    lua_pictures will call this function after all of the pictures have been
    "accumulated".
]]
function sk_scrape_summarize()
    for k, v in next, gCounts do
        io.write("glyph_count ", k, ",frequency ", v * 100 / gTotalCount, "\n")
    end

--[[
    io.write("\n\nFirst glyph spread\n\n")
    for k, v in next, gFirstGlyphs do
        io.write("glyph, ", k, ",count, ", v, "\n")
    end
]]
end

function test_summary()
    io.write("just testing test_summary\n")
end


function tostr(t)
    local str = ""
    for k, v in next, t do
        if #str > 0 then
            str = str .. ", "
        end
        if type(k) == "number" then
            str = str .. "[" .. k .. "] = "
        else
            str = str .. tostring(k) .. " = "
        end
        if type(v) == "table" then
            str = str .. "{ " .. tostr(v) .. " }"
        else
            str = str .. tostring(v)
        end
    end
    return str
end

local total_found = {}    -- accumulate() stores its data in here
local total_total = {}
local canvas        -- holds the current canvas (from startcanvas())

--[[
    startcanvas() is called at the start of each picture file, passing the
    canvas that we will be drawing into, and the name of the file.

    Following this call, there will be some number of calls to accumulate(t)
    where t is a table of parameters that were passed to that draw-op.

        t.verb is a string holding the name of the draw-op (e.g. "drawRect")

    when a given picture is done, we call endcanvas(canvas, fileName)
]]
function sk_scrape_startcanvas(c, fileName)
    canvas = c
end

--[[
    Called when the current canvas is done drawing.
]]
function sk_scrape_endcanvas(c, fileName)
    canvas = nil
end

function increment(table, key)
    table[key] = (table[key] or 0) + 1
end

local drawPointsTable = {}
local drawPointsTable_direction = {}

function sk_scrape_accumulate(t)
    increment(total_total, t.verb)

    local p = t.paint
    if p then
        local pe = p:getPathEffect();
        if pe then
            increment(total_found, t.verb)
        end
    end

    if "drawPoints" == t.verb then
        local points = t.points
        increment(drawPointsTable, #points)
        if 2 == #points then
            if points[1].y == points[2].y then
                increment(drawPointsTable_direction, "hori")
            elseif points[1].x == points[2].x then
                increment(drawPointsTable_direction, "vert")
            else
                increment(drawPointsTable_direction, "other")
            end
        end
    end
end

--[[
    lua_pictures will call this function after all of the pictures have been
    "accumulated".
]]
function sk_scrape_summarize()
    for k, v in next, total_found do
        io.write(k, " = ", v, "/", total_total[k], "\n")
    end
    print("histogram of point-counts for all drawPoints calls")
    print(tostr(drawPointsTable))
    print(tostr(drawPointsTable_direction))
end


clips = 0
draws = 0
clipPaths = 0
drawPaths = 0
swClipPaths = 0
swDrawPaths = 0

skpsTotal = 0
skpsWithPath = 0
skpsWithSWPath = 0

dofile("/tmp/lua-output")

io.write("Number of clips: ", clips, "\n");
io.write("Number of draws: ", draws, "\n");
io.write("Number of clipped paths: ", clipPaths, "\n");
io.write("Number of drawn paths: ", drawPaths, "\n");
io.write("Number of clipped software paths: ", swClipPaths, "\n");
io.write("Number of drawn software paths: ", swDrawPaths, "\n");

io.write("\n")

io.write("Number of SKPs total: ", skpsTotal, "\n")
io.write("Number of SKPs that draw paths: ", skpsWithPath, "\n")
io.write("Number of SKPs that draw SW paths: ", skpsWithSWPath, "\n")

io.write("\n")
io.write("\n")

totalSWPaths = swDrawPaths + swClipPaths
totalPaths = drawPaths + clipPaths

io.write("Percentage of paths needing software: ", (100*(totalSWPaths / totalPaths)), "\n")
io.write("Percentage of draws/clips needing software: ",
         (100*(totalSWPaths / (draws + clips))), "\n")

io.write("\n")

io.write("Percentage of SKPs that draw paths: ", (100*(skpsWithPath / skpsTotal)), "\n")
io.write("Percentage of SKPs that draw SW paths: ", (100*(skpsWithSWPath / skpsTotal)), "\n")


function tostr(t)
    local str = ""
    for k, v in next, t do
        if #str > 0 then
            str = str .. ", "
        end
        if type(k) == "number" then
            str = str .. "[" .. k .. "] = "
        else
            str = str .. tostring(k) .. " = "
        end
        if type(v) == "table" then
            str = str .. "{ " .. tostr(v) .. " }"
        else
            str = str .. tostring(v)
        end
    end
    return str
end

function sk_scrape_startcanvas(c, fileName) end

function sk_scrape_endcanvas(c, fileName) end

local effects = {}

function count_fields(t)
    for k, v in next, t do
        effects[k] = (effects[k] or 0) + 1
    end
end

local total_paints = 0;

function sk_scrape_accumulate(t)
    if (t.paint) then
        total_paints = total_paints + 1
        count_fields(t.paint:getEffects())
    end
end

function sk_scrape_summarize()
    io.write("total paints ", total_paints, " ", tostr(effects), "\n");
end


scrape

function tostr(t)
    local str = ""
    for k, v in next, t do
        if #str > 0 then
            str = str .. ", "
        end
        if type(k) == "number" then
            str = str .. "[" .. k .. "] = "
        else
            str = str .. tostring(k) .. " = "
        end
        if type(v) == "table" then
            str = str .. "{ " .. tostr(v) .. " }"
        else
            str = str .. tostring(v)
        end
    end
    return str
end
local total = {}    -- accumulate() stores its data in here
local canvas        -- holds the current canvas (from startcanvas())
--[[
    startcanvas() is called at the start of each picture file, passing the
    canvas that we will be drawing into, and the name of the file.

    Following this call, there will be some number of calls to accumulate(t)
    where t is a table of parameters that were passed to that draw-op.

        t.verb is a string holding the name of the draw-op (e.g. "drawRect")

    when a given picture is done, we call endcanvas(canvas, fileName)
]]
function sk_scrape_startcanvas(c, fileName)
    canvas = c
end
--[[
    Called when the current canvas is done drawing.
]]
function sk_scrape_endcanvas(c, fileName)
    canvas = nil
end
--[[
    Called with the parameters to each canvas.draw call, where canvas is the
    current canvas as set by startcanvas()
]]
function sk_scrape_accumulate(t)
    local n = total[t.verb] or 0
    total[t.verb] = n + 1
    if false and t.verb == "drawRect" and t.paint:isAntiAlias() then
        local r = t.rect;
        local p = t.paint;
        local c = p:getColor();
    end
    if false and t.verb == "drawPath" then
        local pred, r1, r2, d1, d2 = t.path:isNestedFillRects()

        if pred then
            print("drawRect_Nested", tostr(r1), tostr(r2), d1, d2)
        else
            print("drawPath", "isEmpty", tostring(t.path:isEmpty()),
                    "isRect", tostring(t.path:isRect()), tostr(t.path:getBounds()))
        end
    end
end
--[[
    lua_pictures will call this function after all of the pictures have been
    "accumulated".
]]
function sk_scrape_summarize()
    io.write("\n{ ", tostr(total), " }\n")
end