diff --git a/CMakeLists.txt b/CMakeLists.txt index fbfa2d55a..7a1e45a02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,9 @@ link_directories( ${CMAKE_CURRENT_SOURCE_DIR}/libmoon/deps/luajit/usr/local/lib ) +add_custom_target(tman ALL + COMMAND ${CMAKE_COMMAND} -E create_symlink MoonGen tman) + add_executable(MoonGen ${files}) target_link_libraries(MoonGen ${libraries}) diff --git a/README.md b/README.md index 45c3e1784..d2f4ab606 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,19 @@ MoonGen prints all available ports on startup, so adjust this if necessary. You can also check out the examples of the [libmoon](https://github.com/libmoon/libmoon) project. All libmoon scripts are also valid MoonGen scripts as MoonGen extends libmoon. +## Using tman (task manager) +A newer CLI interface allows to start a number of various tasks at once. Here is the synopsis: +``` + ./build/tman [tman options...] [-- [task1 options...]] [-- [task2 options...]] [-- ...] +``` +Also `./build/tman -h` gives more enhanced help. + +Ordinary task Lua files, however, are incompatible with tman interface. A task file for `tman` has to determine the following objects: +- `function configure(parser)` which describes task's options. +- `function task(taskNum, txInfo, rxInfo, args)` which describes what is being run. Here `txInfo` and `rxInfo` are arrays of RX and TX queues respectively, `args` keeps the parameters of the task manager and the task itself. + +See examples in `examples/tman/`. + # Frequently Asked Questions ### Which NICs do you support? diff --git a/examples/delay-stream.lua b/examples/delay-stream.lua new file mode 100644 index 000000000..ae6001e3a --- /dev/null +++ b/examples/delay-stream.lua @@ -0,0 +1,341 @@ +local mg = require "moongen" +local memory = require "memory" +local device = require "device" +local stats = require "stats" +local log = require "log" +local ip4 = require "proto.ip4" +local libmoon = require "libmoon" +local dpdkc = require "dpdkc" +local ffi = require "ffi" +local syscall = require "syscall" +require "pepperfish" + +function configure(parser) + -- do nothing, just check parse errors + function convertMac_fake(str) + mac = parseMacAddress(str, true) + if not mac then + parser:error("failed to parse MAC "..str) + end + return str + end + + function convertTime(str) + local pattern = "^(%d+)([mu]?s)$" + local _, _, n, unit = string.find(str, pattern) + if not (n and unit) then + parser:error("failed to parse time '"..str.."', it should match '"..pattern.."' pattern") + end + return {n=tonumber(n), unit=unit} + end + + parser:description("Redirects stream between devices, optionally mangling and delaying data.") + parser:argument("srcDev", "Source device."):convert(tonumber) + parser:argument("dstDev", "Destination device."):convert(tonumber) + parser:option("-n", "Number of threads."):convert(tonumber):default(1) + parser:option("-d --destination", "Rewrite destination IP.") + parser:option("-m --ethDst", "Rewrite destination MAC."):convert(convertMac_fake) + parser:option("--delay", "Delay in us."):convert(tonumber):default(0) + parser:option("--buf", "Buffer size."):convert(tonumber):default(63) + parser:option("-l --max-pktlen", "Maximum packet length."):convert(tonumber):default(60) + parser:flag("--feed-self", "Do not drop traffic generated by ourselves.") + parser:flag("--fake-write", "Do not actually issue traffic.") + parser:flag("--debug", "Debug ring buffer.") + parser:flag("--profile-read", "Turn on profiling for reading.") + parser:flag("--profile-write", "Turn no profiling for writing.") +end + +local shm_filename = "/MoonGen" + +local function setup(isMaster, taskNum, args) + if args.debug then + log:setLevel("DEBUG") + end + + local cfg = {} + local shm_filename = shm_filename .. "-" .. tostring(taskNum) + + cfg.chunkSize = args.buf + 1 + cfg.packetSpace = args.max_pktlen + 4 + local delay = args.delay; if delay <= 0 then delay = 1 end + local extra = 10.0 -- just in case + local _, ringLogsize = math.frexp(14.88 * delay * extra / args.buf) + if ringLogsize < 2 then ringLogsize = 2 end + cfg.ringSize = bit.lshift(1, ringLogsize) + cfg.ringLogsize = ringLogsize + if isMaster and taskNum == 1 then + log:info("buf size: %d, ringSize: %d, e: %d", args.buf, cfg.ringSize, ringLogsize) + end + cfg.ringDblSize = cfg.ringSize * 2 + cfg.ringDblMask = 2 * cfg.ringDblSize - 1 + + local oflags + if isMaster then oflags = "CREAT,RDWR,EXCL" else oflags = "RDWR" end + + local shm_fd, err = syscall.shm_open(shm_filename, oflags, "RUSR,WUSR") + if not shm_fd then + if err.EXIST then + -- try to unlink and retry to open + syscall.shm_unlink(shm_filename) + shm_fd, err = syscall.shm_open(shm_filename, oflags, "RUSR,WUSR") + end + if not shm_fd then + errorf("shm_open(\"%s\", ...) failed: %s", shm_filename, err) + end + end + local ringRawSize = cfg.ringSize * cfg.chunkSize * cfg.packetSpace + local shm_size = ringRawSize + 8 + local ok, err = syscall.ftruncate(shm_fd, shm_size) + if not ok then + errorf("ftruncate(...) failed: %s", err) + end + local ptr, err = syscall.mmap(nil, shm_size, "READ,WRITE", "SHARED", shm_fd, 0) + if not ptr then + errorf("nmap(...) failed: %s", err) + end + cfg.ringBufferRaw = ffi.cast("uint8_t*", ptr) + cfg.ax_ptr = ffi.cast("volatile uint32_t*", cfg.ringBufferRaw+ringRawSize) + cfg.bx_ptr = ffi.cast("volatile uint32_t*", cfg.ringBufferRaw+ringRawSize+4) + + if isMaster then + cfg.ax_ptr[0] = 0 + cfg.bx_ptr[0] = 0 + cfg.cleanupFunc = function() + syscall.munmap(ptr, shm_size) + syscall.close(shm_fd) + syscall.shm_unlink(shm_filename) + end + else + cfg.cleanupFunc = function() syscall.munmap(ptr, shm_size); syscall.close(shm_fd) end + end + + return cfg +end + +function master(args) + if bit.band(args.buf, args.buf + 1) ~= 0 then + log:warn("Recommended buffer size is 2^n-1") + end + + local n = args.n + local cfgs = {} + for i = 1, n do + cfgs[i] = setup(true, i, args) + end + + local srcDev, dstDev + if args.srcDev == args.dstDev then + srcDev = device.config{port = args.srcDev, rxQueues = n, txQueues = n, rssQueues = n} + dstDev = srcDev + srcDev:wait() + else + srcDev = device.config{port = args.srcDev, rxQueues = n, rssQueues = n} + dstDev = device.config{port = args.dstDev, txQueues = n} + srcDev:wait() + dstDev:wait() + end + + for i = 0, n - 1 do + mg.startTask("task_read", i+1, srcDev:getRxQueue(i), dstDev:getMac(true), args) + mg.startTask("task_write", i+1, dstDev:getTxQueue(i), args) + end + + mg.waitForTasks() + for _, cfg in ipairs(cfgs) do cfg.cleanupFunc() end +end + +local function gettimeofday_n() + local sec, usec = gettimeofday() + return tonumber(sec), tonumber(usec) +end + +local zero16 = hton16(0) + +function task_read(taskNum, rxQ, ethSelf, args) + local cfg = setup(false, taskNum, args) + local ringDblMask = cfg.ringDblMask + local ax_ptr = cfg.ax_ptr + local bx_ptr = cfg.bx_ptr + local ringBufferRaw = cfg.ringBufferRaw + local chunkSize = cfg.chunkSize + local packetSpace = cfg.packetSpace + local ringMask = cfg.ringSize - 1 + local ringDblSize = cfg.ringDblSize + + local max_pktlen, feed_self = args.max_pktlen, args.feed_self + + local rxStats = stats:newDevRxCounter(rxQ, "plain") + local manStats = stats:newManualRxCounter("man:" .. tostring(taskNum)) + local timesBufferFull = 0 + + local mem = memory.createMemPool() + local bufs_read = mem:bufArray(chunkSize - 1) + + -- ring buffer pointers (not using "tx", "rx" due to confusion), ax is to be "greater" than bx + local ax, bx = 0, 0 + + if args.profile_read then + profiler = newProfiler("call") + profiler:start() + end + + while mg.running() do + bx = bx_ptr[0] + if bit.band(ax - bx, ringDblSize) == 0 -- ring not full + then + local sec, usec = gettimeofday_n() + local rx = rxQ:recv(bufs_read) + manStats:update(rx, 0) + + local chunkIdx = bit.band(ax, ringMask) + local j = 0 + for i = 1, rx do + local buf = bufs_read.array[i-1] + local pkt = buf:getTcpPacket(ipv4) + local pkt_raw = ringBufferRaw + ((chunkIdx * chunkSize + j) * packetSpace) + local bufSize = buf:getSize() + if bufSize <= max_pktlen and (feed_self or pkt.eth.src:get() ~= ethSelf) + then + ffi.cast("uint32_t*", pkt_raw)[0] = bufSize + ffi.copy(pkt_raw + 4, buf:getRawPacket(), bufSize) + j = j + 1 + end + end + + local meta_raw = ffi.cast("volatile uint32_t*", ringBufferRaw + (((chunkIdx + 1) * chunkSize - 1) * packetSpace)) + meta_raw[0], meta_raw[1], meta_raw[2] = rx, sec, usec + log:debug("READ-%d %03x, %03x", taskNum, ax, bx) + ax = bit.band(ax + 1, ringDblMask) + ax_ptr[0] = ax + + rxStats:update() + bufs_read:freeAll() + else + if timesBufferFull == 0 then + log:warn("Buffer is full") + end + timesBufferFull = timesBufferFull + 1 + end + end + + if args.profile_read then + profiler:stop() + local outfile = io.open( "profile-read.txt", "w+" ) + profiler:report(outfile) + outfile:close() + end + + rxStats:finalize() + manStats:finalize() + if timesBufferFull ~= 0 then + log:warn("Buffer was full %d times", timesBufferFull) + end + cfg.cleanupFunc() +end + +function task_write(taskNum, txQ, args) + local cfg = setup(false, taskNum, args) + local ringDblMask = cfg.ringDblMask + local ax_ptr = cfg.ax_ptr + local bx_ptr = cfg.bx_ptr + local ringBufferRaw = cfg.ringBufferRaw + local chunkSize = cfg.chunkSize + local packetSpace = cfg.packetSpace + local ringMask = cfg.ringSize - 1 + + local delay, max_pktlen, fake_write = args.delay, args.max_pktlen, args.fake_write + + local ipDst = args.destination and parseIPAddress(args.destination) + local ethDst = args.ethDst and parseMacAddress(args.ethDst, true) + local ethSrc = txQ.dev:getMac(true) + + local txStats = stats:newDevTxCounter(txQ, "plain") + + local mem = memory.createMemPool() + local bufs_write = mem:bufArray(chunkSize - 1) + + -- ring buffer pointers (not using "tx", "rx" due to confusion), ax is to be "greater" than bx + local ax, bx = 0, 0 + + local function do_write() + ax = ax_ptr[0] + while bit.band(ax - bx, ringDblMask) ~= 0 -- ring not empty + do + if not fake_write then + local chunkIdx = bit.band(bx, ringMask) + local meta_raw = ffi.cast("volatile uint32_t*", ringBufferRaw + (((chunkIdx + 1) * chunkSize - 1) * packetSpace)) + local sec, usec = gettimeofday_n() + local delta_usec = (sec - meta_raw[1]) * 1000000 + (usec - meta_raw[2]) + + if delta_usec > delay then + log:debug("WRITE-%d %03x, %03x, delta: %d", taskNum, ax, bx, delta_usec) + + local nRecvd = meta_raw[0] + if nRecvd > chunkSize - 1 then + nRecvd = chunkSize - 1 + log:warn("Probably garbage in ring buffer (nRecvd): %d > %d", nRecvd, chunkSize-1) + end + bufs_write:resize(nRecvd) + bufs_write:alloc(max_pktlen) + + local i = 0 + for j = 0, nRecvd-1 do + local pkt_raw = ringBufferRaw + ((chunkIdx * chunkSize + j) * packetSpace) + local pktSize = ffi.cast("uint32_t*", pkt_raw)[0] + if pktSize ~= 0 then + i = i + 1 + local buf = bufs_write.array[i-1] + buf:setSize(pktSize) + ffi.copy(buf:getData(), pkt_raw+4, pktSize) + + local pkt = buf:getTcpPacket(ipv4) + if pkt.ip4:getProtocol() == ip4.PROTO_TCP then + if ethDst then + pkt.eth.dst:set(ethDst) + pkt.eth.src:set(ethSrc) + end + if ipDst then pkt.ip4.dst:set(ipDst) end + --pkt.ip4:setChecksum(0) + pkt.ip4.cs = zero16 -- FIXME: setChecksum() is extremely slow + end + end + end + if i ~= 0 then + bufs_write:resize(i) + bufs_write:offloadTcpChecksums(ipv4) + txQ:send(bufs_write) + end + txStats:update() + bx = bit.band(bx + 1, ringDblMask) + bx_ptr[0] = bx + else + if delta_usec < 0 then + log:warn("Oops, something wrong with time") + end + return + end + else + bx = bit.band(bx + 1, ringDblMask) + bx_ptr[0] = bx + end + end + end + + if args.profile_write then + profiler = newProfiler("call") + profiler:start() + end + + while mg.running() do + do_write() + end + if args.profile_write then + profiler:stop() + local outfile = io.open( "profile-write.txt", "w+" ) + profiler:report(outfile) + outfile:close() + end + + txStats:finalize() + cfg.cleanupFunc() +end diff --git a/examples/icmp-arp-responder.lua b/examples/icmp-arp-responder.lua index 98ad07098..a86a61e7f 100644 --- a/examples/icmp-arp-responder.lua +++ b/examples/icmp-arp-responder.lua @@ -1,3 +1,4 @@ +local mg = require "moongen" local dpdk = require "dpdk" local memory = require "memory" local device = require "device" @@ -9,24 +10,18 @@ local ip = require "proto.ip4" local icmp = require "proto.icmp" -function master(funny, port, ...) - if funny and funny ~= "--do-funny-things" then - return master(nil, funny, port, ...) - end - port = tonumber(port) - if not port or select("#", ...) == 0 or ... == nil then - log:info("usage: [--do-funny-things] port ip [ip...]") - return - end - - local dev = device.config(port, 2, 2) - device.waitForLinks() - - dpdk.launchLua(arp.arpTask, { - { rxQueue = dev:getRxQueue(1), txQueue = dev:getTxQueue(1), ips = { ... } } - }) +function configure(parser) + parser:description("ICMP ARP responder") + parser:argument("dev", "Device number."):convert(tonumber) + parser:flag("--do-funny-things") +end + - pingResponder(dev, funny) +function master(args) + local dev = device.config{port = args.dev, txQueues = 1, rxQueues = 1} + dev:wait() + mg.startTask("pingResponder", dev, args.do_funny_things) + mg.waitForTasks() end local DIGITS = { 1, 8 } @@ -65,7 +60,7 @@ function pingResponder(dev, funny) local rxMem = memory.createMemPool() local rxBufs = rxMem:bufArray(1) - while dpdk.running() do + while mg.running() do rx = rxQueue:recv(rxBufs) if rx > 0 then local buf = rxBufs[1] diff --git a/examples/l3-tcp-syn-ack-flood.lua b/examples/l3-tcp-syn-ack-flood.lua new file mode 100644 index 000000000..69d577b62 --- /dev/null +++ b/examples/l3-tcp-syn-ack-flood.lua @@ -0,0 +1,275 @@ +local mg = require "moongen" +local memory = require "memory" +local device = require "device" +local stats = require "stats" +local log = require "log" +local ip4 = require "proto.ip4" +local libmoon = require "libmoon" + + +function configure(parser) + -- do nothing, just check parse errors + function convertMac_fake(str) + mac = parseMacAddress(str, true) + if not mac then + parser:error("failed to parse MAC "..str) + end + return str + end + + function convertTime(str) + local pattern = "^(%d+)([mu]?s)$" + local _, _, n, unit = string.find(str, pattern) + if not (n and unit) then + parser:error("failed to parse time '"..str.."', it should match '"..pattern.."' pattern") + end + return {n=tonumber(n), unit=unit} + end + + parser:description("Generates TCP SYN flood from varying source IPs, supports both IPv4 and IPv6") + parser:argument("dev", "Devices to transmit from."):args("*"):convert(tonumber) + parser:option("-r --rate", "Transmit rate in Mbit/s."):default(10000):convert(tonumber) + parser:option("-i --ip", "Source IP (IPv4 or IPv6)."):default("10.0.0.1") + parser:option("-d --destination", "Destination IP (IPv4 or IPv6).") + parser:option("-f --flows", "Number of different IPs to use."):default(100):convert(tonumber) + parser:option("-s --synq", "Number of SYN queues."):default(0):convert(tonumber) + parser:option("-x --synackq", "Number of SYN-ACK queues."):default(0):convert(tonumber) + parser:option("-a --ackq", "Number of ACK queues."):default(0):convert(tonumber) + parser:flag("-c", "Add RX counter.") + parser:option("-m --ethDst", "Destination MAC, this option may be repeated."):count("*"):convert(convertMac_fake) + parser:option("--ipg", "Inter-packet gap, time units (s, ms, us) must be specified."):convert(convertTime) +end + +function master(args) + if args.synq == 0 and args.ackq == 0 and args.synackq == 0 and not args.c then + log:fatal("Use at least one queue") + end + + local txQueues = args.synq + args.ackq + args.synackq + local rxQueues = args.ackq + args.synackq + if args.c then rxQueues = rxQueues + 1 end + + for i, dev in ipairs(args.dev) do + if rxQueues == 0 then rxQueues = 1 end + if txQueues == 0 then txQueues = 1 end + local dev = device.config{port = dev, txQueues = txQueues, rxQueues = rxQueues, rssQueues = rxQueues} + dev:wait() + + for i = 0, args.ackq-1 do + local txQ = dev:getTxQueue(i) + local rxQ = dev:getRxQueue(i) + txQ:setRate(args.rate) + mg.startTask("replySlave", false, txQ, rxQ) + end + + for i = args.ackq, args.ackq+args.synackq-1 do + local txQ = dev:getTxQueue(i) + local rxQ = dev:getRxQueue(i) + txQ:setRate(args.rate) + mg.startTask("replySlave", true, txQ, rxQ) + end + + for i = args.ackq+args.synackq, args.ackq+args.synackq+args.synq-1 do + local txQ = dev:getTxQueue(i) + txQ:setRate(args.rate) + mg.startTask("synSlave", txQ, args.ip, args.flows, args.destination, args.ethDst, args.ipg) + end + + if args.c then + mg.startTask("rxCount", dev:getRxQueue(rxQueues-1)) + end + end + mg.waitForTasks() +end + +local zero16 = hton16(0) + +function replySlave(synack, txQ, rxQ) + if synack then + print("replySlave synack") + else + print("replySlave -") + end + local txBufs = memory.bufArray(128) + local rxBufs = memory.bufArray(128) + local txStats = stats:newDevTxCounter(txQ, "plain") + local rxStats = stats:newDevRxCounter(rxQ, "plain") + + while mg.running() do + local tx = 0 + local rx = rxQ:recv(rxBufs) + for i = 1, rx do + local buf = rxBufs[i] + -- alter buf + local pkt = buf:getTcpPacket(ipv4) + if pkt.ip4:getProtocol() == ip4.PROTO_TCP and + pkt.tcp:getSyn() and + (pkt.tcp:getAck() or synack) + then + -- print(string.format("RECV %d %d\n", rx, tx)) + local seq = pkt.tcp:getSeqNumber() + local ack = pkt.tcp:getAckNumber() + + if synack then + pkt.tcp:setAck() + pkt.tcp:setAckNumber(seq+1) + pkt.tcp:setSeqNumber(ack) + else + pkt.tcp:unsetSyn() + pkt.tcp:setAckNumber(seq+1) + pkt.tcp:setSeqNumber(ack) + end + + local tmp = pkt.ip4.src:get() + pkt.ip4.src:set(pkt.ip4.dst:get()) + pkt.ip4.dst:set(tmp) + + local tmp1 = pkt.eth.dst:get() + pkt.eth.dst:set(pkt.eth.src:get()) + pkt.eth.src:set(tmp1) + + local tmp2 = pkt.tcp:getDstPort() + pkt.tcp:setDstPort(pkt.tcp:getSrcPort()) + pkt.tcp:setSrcPort(tmp2) + + --pkt.ip4:setChecksum(0) + pkt.ip4.cs = zero16 -- FIXME: setChecksum() is extremely slow + + tx = tx + 1 + txBufs[tx] = buf + end + end + if tx > 0 then + txBufs:resize(tx) + --offload checksums to NIC + txBufs:offloadTcpChecksums(ipv4) + txQ:send(txBufs) + + rxStats:update() + txStats:update() + end + end + rxStats:finalize() + txStats:finalize() +end + +function synSlave(queue, minA, numIPs, dest, ethDst_str, ipg) + ethDst = {} + for i,x in ipairs(ethDst_str) do + ethDst[i] = parseMacAddress(x, true) + end + + local ipgSleepFunc = function() end + if ipg and ipg.n ~= 0 then + if ipg.unit == "us" then + ipgSleepFunc = function() libmoon.sleepMicrosIdle(ipg.n) end + elseif ipg.unit == "ms" then + ipgSleepFunc = function() libmoon.sleepMillisIdle(ipg.n) end + elseif ipg.unit == "s" then + ipgSleepFunc = function() libmoon.sleepMillisIdle(ipg.n * 1000) end + end + end + + --- parse and check ip addresses + local minIP, ipv4 = parseIPAddress(minA) + if minIP then + log:info("Detected an %s address.", minIP and "IPv4" or "IPv6") + else + log:fatal("Invalid minIP: %s", minA) + end + + -- min TCP packet size for IPv6 is 74 bytes (+ CRC) + local packetLen = ipv4 and 60 or 74 + + local mem = memory.createMemPool(function(buf) + buf:getTcpPacket(ipv4):fill{ + ethSrc = queue, + ethDst = "90:e2:ba:7d:85:6c", + ip4Dst = dest, + ip6Dst = dest, + tcpSyn = 1, + tcpSeqNumber = 1, + tcpWindow = 10, + pktLength = packetLen} + -- FIXME: workaround + if ethDst[1] then + buf:getTcpPacket(ipv4).eth:setDst(ethDst[1]) + end + end) + + local updateEthDst = function(pkt) end + if #ethDst > 1 then + local idx = nil + local dst + updateEthDst = function(pkt) + idx, dst = next(ethDst, idx) + if not idx then + idx = nil + idx, dst = next(ethDst, idx) + end + pkt.eth:setDst(dst) + end + end + + local bufs = mem:bufArray(128) + local counter = 0 + local portCounter = 0 + local c = 0 + + local txStats = stats:newDevTxCounter(queue, "plain") + while mg.running() do + -- fill packets and set their size + bufs:alloc(packetLen) + for i, buf in ipairs(bufs) do + local pkt = buf:getTcpPacket(ipv4) + + pkt.tcp:setDstPort(80) + pkt.ip4.src:set(minIP) + updateEthDst(pkt) + --increment IP + if ipv4 then + pkt.ip4.src:set(minIP) + pkt.ip4.src:add(counter) + else + pkt.ip6.src:set(minIP) + pkt.ip6.src:add(counter) + end + counter = incAndWrap(counter, numIPs) + + pkt.tcp:setSrcPort(1000+portCounter) + portCounter = incAndWrap(portCounter, 100) + + -- dump first 3 packets + if c < 3 then + buf:dump() + c = c + 1 + end + end + --offload checksums to NIC + bufs:offloadTcpChecksums(ipv4) + + queue:send(bufs) + txStats:update() + ipgSleepFunc() + end + txStats:finalize() +end + +function rxCount(rxQ) + print("rxCount") + local rxCtr = stats:newDevRxCounter(rxQ) + local rxBufs = memory.bufArray(128) + while mg.running() do + local rx = rxQ:recv(rxBufs) + rxBufs:free(rx) + rxCtr:update() + end + rxCtr:finalize() +end + +defaults = {rx_queues = 0, tx_queues = 1} + +function task(taskNum, txInfo, rxInfo, args) + local txQ = txInfo[1].queue + synSlave(txQ, args.ip, args.flows, args.destination, args.ethDst, args.ipg) +end diff --git a/examples/l3-tcp-syn-flood.lua b/examples/l3-tcp-syn-flood.lua index fd2a3e2b5..d640bb5d1 100644 --- a/examples/l3-tcp-syn-flood.lua +++ b/examples/l3-tcp-syn-flood.lua @@ -11,14 +11,23 @@ function configure(parser) parser:option("-i --ip", "Source IP (IPv4 or IPv6)."):default("10.0.0.1") parser:option("-d --destination", "Destination IP (IPv4 or IPv6).") parser:option("-f --flows", "Number of different IPs to use."):default(100):convert(tonumber) + parser:option("-q --queues", "Number of tx queues."):default(1):convert(tonumber) end function master(args) for i, dev in ipairs(args.dev) do - local dev = device.config{port = dev} + local dev = device.config{port = dev, txQueues = args.queues} dev:wait() - dev:getTxQueue(0):setRate(args.rate) - mg.startTask("loadSlave", dev:getTxQueue(0), args.ip, args.flows, args.destination) + + local nQ = dev:getInfo().nb_tx_queues -- should be the same as args.queues + if nQ < args.queues then + print(string.format("Warning: requested %d queues, but only %d available", args.queues, nQ)) + end + for i = 0, nQ-1 do + local txQ = dev:getTxQueue(i) + txQ:setRate(args.rate) + mg.startTask("loadSlave", txQ, args.ip, args.flows, args.destination) + end end mg.waitForTasks() end diff --git a/examples/pcap/rx-to-pcap.lua b/examples/pcap/rx-to-pcap.lua new file mode 100644 index 000000000..7b4bd2832 --- /dev/null +++ b/examples/pcap/rx-to-pcap.lua @@ -0,0 +1,76 @@ +--! @file rx-to-pcap.lua +--! @brief Capture to Pcap with software timestamping + +local mg = require "moongen" +local memory = require "memory" +local device = require "device" +local log = require "log" +local ts = require "timestamping" +local pcap = require "pcap" +local ffi = require "ffi" +local stats = require "stats" + +function configure(parser) + parser:description("Reads packets from device and writes them to pcap files.") + parser:argument("dev", "Devices to receive from."):args("+"):convert(tonumber) + parser:option("-s --sink", "File prefix to write pcap data: 'PREFIX-portNum-queueNum.pcap'") + parser:option("-q --queues", "Number of RX queues."):default(1):convert(tonumber) + parser:option("-m --maxp", "Reads at most maxp packets or forever if maxp == 0."):default(0):convert(tonumber) + +end + +function master(args) + for i, dev in ipairs(args.dev) do + local rxDev = device.config{ port = dev, rxQueues = args.queues } + local maxp = args.maxp / args.queues + rxDev:wait() + for i = 0, args.queues-1 do + local sink = args.sink and args.sink..dev..'-'..i..'.pcap' + mg.startTask("pcapSinkSlave", rxDev:getRxQueue(i), sink, maxp) + end + end + + mg.waitForTasks() +end + + + +--! @brief: receive and store packets with software timestamps +function pcapSinkSlave(queue, sink, maxp) + print('pcapSinkSlave is running') + local numbufs = (maxp == 0) and 100 or math.min(100, maxp) + local bufs = memory.bufArray(numbufs) + + local writer = sink and pcap:newWriter(sink) + local ctr = stats:newDevRxCounter(queue, "plain") + local pkts = 0 + while mg.running() and (maxp == 0 or pkts < maxp) do + local rxnum = (maxp == 0) and #bufs or math.min(#bufs, maxp - pkts) + local rx = queue:tryRecv(bufs, rxnum) + local batchTime = mg.getTime() + for i = 1, rx do + local buf = bufs[i] + if writer then + writer:writeBuf(batchTime, buf, 0) + end + if handleArp and buf:getEthernetPacket().eth:getType() == eth.TYPE_ARP then + -- inject arp packets to the ARP task + -- this is done this way instead of using filters to also dump ARP packets here + arp.handlePacket(buf) + else + -- do not free packets handlet by the ARP task, this is done by the arp task + buf:free() + end + end + pkts = pkts + rx + ctr:update() + bufs:free(rx) + end + print('pcapSinkSlave terminated after receiving '..pkts.." packets") + bufs:freeAll() + + ctr:finalize() + if writer then + writer:close() + end +end diff --git a/examples/rx-rate.lua b/examples/rx-rate.lua new file mode 100644 index 000000000..404f14759 --- /dev/null +++ b/examples/rx-rate.lua @@ -0,0 +1,44 @@ +local mg = require "moongen" +local memory = require "memory" +local device = require "device" +local stats = require "stats" +local utils = require "utils" +local log = require "log" + +local arp = require "proto.arp" +local ip = require "proto.ip4" +local icmp = require "proto.icmp" + + +function master(...) + if not ... then + log:fatal("usage: port:ip [port:ip...]") + end + for _,arg in ipairs({...}) do + port, ip = string.gmatch(arg, "(%g+):(%g+)")() + port = tonumber(port) + if not port or not ip then + log:fatal("usage: port:ip [port:ip...]") + end + + local dev = device.config{ port = port, rxQueues = 2, txQueues = 2 } + device.waitForLinks() + + mg.startTask(arp.arpTask, { + { rxQueue = dev:getRxQueue(1), txQueue = dev:getTxQueue(1), ips = { ip } } + }) + + mg.startTask("rxCount", dev) + end + mg.waitForTasks() +end + + +function rxCount(dev) + local rxCtr = stats:newDevRxCounter(dev) + while mg.running() do + rxCtr:update() + end + rxCtr:finalize() +end + diff --git a/examples/tman/l3-tcp-syn-reply.lua b/examples/tman/l3-tcp-syn-reply.lua new file mode 100644 index 000000000..7bbb84c75 --- /dev/null +++ b/examples/tman/l3-tcp-syn-reply.lua @@ -0,0 +1,89 @@ +local mg = require "moongen" +local memory = require "memory" +local stats = require "stats" +local log = require "log" +local ip4 = require "proto.ip4" +local libmoon = require "libmoon" + +defaults = {rx_queues = 1, tx_queues = 1} + +function configure(parser) + parser:description("Depending on mode performs 2nd either 3rd step in 3-way TCP handshake") + parser:command("SYNACK SYN+ACK synack 2", "Reply SYN -> SYN+ACK, i.e. perform the 2nd step of TCP handshake") + parser:command("ACK ack 3", "Reply SYN+ACK -> ACK, i.e. perform the 3nd step of TCP handshake") +end + +local zero16 = hton16(0) + +function task(taskNum, txInfo, rxInfo, args) + local txQ = txInfo[1].queue + local rxQ = rxInfo[1].queue + local synack = args.SYNACK + if synack then + print("Running in SYN+ACK mode") + else + print("Running in ACK mode") + end + local txBufs = memory.bufArray(tx_buf) + local rxBufs = memory.bufArray(rx_buf) + local txStats = stats:newDevTxCounter(txQ, "plain") + local rxStats = stats:newDevRxCounter(rxQ, "plain") + + while mg.running() do + local tx = 0 + local rx = rxQ:recv(rxBufs) + for i = 1, rx do + local buf = rxBufs[i] + -- alter buf + local pkt = buf:getTcpPacket(ipv4) + if pkt.ip4:getProtocol() == ip4.PROTO_TCP and + pkt.tcp:getSyn() and + (pkt.tcp:getAck() or synack) + then + -- print(string.format("RECV %d %d\n", rx, tx)) + local seq = pkt.tcp:getSeqNumber() + local ack = pkt.tcp:getAckNumber() + + if synack then + pkt.tcp:setAck() + pkt.tcp:setAckNumber(seq+1) + pkt.tcp:setSeqNumber(ack) + else + pkt.tcp:unsetSyn() + pkt.tcp:setAckNumber(seq+1) + pkt.tcp:setSeqNumber(ack) + end + + local tmp = pkt.ip4.src:get() + pkt.ip4.src:set(pkt.ip4.dst:get()) + pkt.ip4.dst:set(tmp) + + local tmp1 = pkt.eth.dst:get() + pkt.eth.dst:set(pkt.eth.src:get()) + pkt.eth.src:set(tmp1) + + local tmp2 = pkt.tcp:getDstPort() + pkt.tcp:setDstPort(pkt.tcp:getSrcPort()) + pkt.tcp:setSrcPort(tmp2) + + --pkt.ip4:setChecksum(0) + pkt.ip4.cs = zero16 -- FIXME: setChecksum() is extremely slow + + tx = tx + 1 + txBufs[tx] = buf + end + end + rxBufs:freeAfter(rx) + if tx > 0 then + txBufs:resize(tx) + --offload checksums to NIC + txBufs:offloadTcpChecksums(ipv4) + txQ:send(txBufs) + + rxStats:update() + txStats:update() + end + end + rxStats:finalize() + txStats:finalize() +end diff --git a/examples/tman/l3-tcp-syn.lua b/examples/tman/l3-tcp-syn.lua new file mode 100644 index 000000000..0901c4537 --- /dev/null +++ b/examples/tman/l3-tcp-syn.lua @@ -0,0 +1,138 @@ +local mg = require "moongen" +local memory = require "memory" +local stats = require "stats" +local log = require "log" +local ip4 = require "proto.ip4" +local libmoon = require "libmoon" + +defaults = {rx_queues = 0, tx_queues = 1} + +function configure(parser) + -- do nothing, just check parse errors + function convertMac_fake(str) + mac = parseMacAddress(str, true) + if not mac then + parser:error("failed to parse MAC "..str) + end + return str + end + + function convertTime(str) + local pattern = "^(%d+)([mu]?s)$" + local _, _, n, unit = string.find(str, pattern) + if not (n and unit) then + parser:error("failed to parse time '"..str.."', it should match '"..pattern.."' pattern") + end + return {n=tonumber(n), unit=unit} + end + + parser:description("Generates TCP SYN flood from varying source IPs") + parser:option("--ipSrc", "Initial source IP."):default("10.0.0.1") + parser:option("--ipDst", "Destination IP.") + parser:option("-f --flows", "Number of different IPs to use."):default(100):convert(tonumber) + parser:option("-m --ethDst", "Destination MAC, this option may be repeated."):count("*"):convert(convertMac_fake) +end + +function task(taskNum, txInfo, rxInfo, args) + local txQ = txInfo[1].queue + local minA, numIPs, dest, ethDst_str, ipg = args.ipSrc, args.flows, args.ipDst, args.ethDst, args.ipg + ethDst = {} + for i,x in ipairs(ethDst_str) do + ethDst[i] = parseMacAddress(x, true) + end + + local ipgSleepFunc = function() end + if ipg and ipg.n ~= 0 then + if ipg.unit == "us" then + ipgSleepFunc = function() libmoon.sleepMicrosIdle(ipg.n) end + elseif ipg.unit == "ms" then + ipgSleepFunc = function() libmoon.sleepMillisIdle(ipg.n) end + elseif ipg.unit == "s" then + ipgSleepFunc = function() libmoon.sleepMillisIdle(ipg.n * 1000) end + end + end + + --- parse and check ip addresses + local minIP, ipv4 = parseIPAddress(minA) + if minIP then + log:info("Detected an %s address.", minIP and "IPv4" or "IPv6") + else + log:fatal("Invalid minIP: %s", minA) + end + + -- min TCP packet size for IPv6 is 74 bytes (+ CRC) + local packetLen = ipv4 and 60 or 74 + + local mem = memory.createMemPool(function(buf) + buf:getTcpPacket(ipv4):fill{ + ethSrc = txQ, + ethDst = "90:e2:ba:7d:85:6c", + ip4Dst = dest, + ip6Dst = dest, + tcpSyn = 1, + tcpSeqNumber = 1, + tcpWindow = 10, + pktLength = packetLen} + -- FIXME: workaround + if ethDst[1] then + buf:getTcpPacket(ipv4).eth:setDst(ethDst[1]) + end + end) + + local updateEthDst = function(pkt) end + if #ethDst > 1 then + local idx = nil + local dst + updateEthDst = function(pkt) + idx, dst = next(ethDst, idx) + if not idx then + idx = nil + idx, dst = next(ethDst, idx) + end + pkt.eth:setDst(dst) + end + end + + local bufs = mem:bufArray(args.tx_buf) + local counter = 0 + local portCounter = 0 + local c = 0 + + local txStats = stats:newDevTxCounter(txQ, "plain") + while mg.running() do + -- fill packets and set their size + bufs:alloc(packetLen) + for i, buf in ipairs(bufs) do + local pkt = buf:getTcpPacket(ipv4) + + pkt.tcp:setDstPort(80) + pkt.ip4.src:set(minIP) + updateEthDst(pkt) + --increment IP + if ipv4 then + pkt.ip4.src:set(minIP) + pkt.ip4.src:add(counter) + else + pkt.ip6.src:set(minIP) + pkt.ip6.src:add(counter) + end + counter = incAndWrap(counter, numIPs) + + pkt.tcp:setSrcPort(1000+portCounter) + portCounter = incAndWrap(portCounter, 100) + + -- dump first 3 packets + if c < 3 then + buf:dump() + c = c + 1 + end + end + --offload checksums to NIC + bufs:offloadTcpChecksums(ipv4) + + txQ:send(bufs) + txStats:update() + ipgSleepFunc() + end + txStats:finalize() +end diff --git a/examples/tman/rx-rate.lua b/examples/tman/rx-rate.lua new file mode 100644 index 000000000..762c00a40 --- /dev/null +++ b/examples/tman/rx-rate.lua @@ -0,0 +1,21 @@ +local mg = require "moongen" +local memory = require "memory" +local stats = require "stats" + +defaults = {rx_queues = 1, tx_queues = 0} + +function task(taskNum, txInfo, rxInfo, args) + local rxQ = rxInfo[1].queue + local rxCtr = stats:newDevRxCounter(rxQ) + local rx_buf = args.rx_buf + if not rx_buf then rx_buf = 128 end + local rxBufs = memory.bufArray(rx_buf) + + while mg.running() do + local rx = rxQ:recv(rxBufs) + rxBufs:freeAll() + rxCtr:update() + end + rxCtr:finalize() +end + diff --git a/lua/moongen-main.lua b/lua/moongen-main.lua index 1044aef7b..3acff4005 100644 --- a/lua/moongen-main.lua +++ b/lua/moongen-main.lua @@ -7,6 +7,229 @@ require "software-timestamps" require "crc-ratecontrol" require "software-ratecontrol" +-- set up logger before doing anything else +local log = require "log" +-- set log level +log:setLevel("INFO") +-- enable logging to file +--log:fileEnable() + +-- globally available utility functions +require "utils" + +local libmoon = require "libmoon" +local dpdk = require "dpdk" +local dpdkc = require "dpdkc" +local device = require "device" +local stp = require "StackTracePlus" +local ffi = require "ffi" +local memory = require "memory" +local serpent = require "Serpent" +local argparse = require "argparse" +local mg = require "moongen" + -- libmoon main, contains main() require "main" +local libmoon_main = main + +local function getStackTrace(err) + print(red("[FATAL] Lua error in task %s", LIBMOON_TASK_NAME)) + print(stp.stacktrace(err, 2)) +end + +local function run(file, ...) + local script, err = loadfile(file) + if not script then + error(err) + end + return xpcall(script, getStackTrace, ...) +end + +local function dashdashSplitArgs(...) + local args = { ... } + local multiargs = {{}} + local k = 1 + for i, arg in ipairs(args) do + if arg == "--" then + k = k + 1 + multiargs[k] = {} + else + table.insert(multiargs[k], arg) + end + end + return multiargs +end + +local function configure_common(parser, defaults0, defaults1) + function convertTime(str) + local pattern = "^(%d+)([mu]?s)$" + local _, _, n, unit = string.find(str, pattern) + if not (n and unit) then + parser:error("failed to parse time '"..str.."', it should match '"..pattern.."' pattern") + end + return {n=tonumber(n), unit=unit} + end + + local defaults = {} + for _, k in ipairs{"rx_queues", "tx_queues"} do + defaults[k] = defaults1[k] or defaults0[k] + end + + parser:option("--buf", "RX/TX buffer size."):convert(tonumber) + parser:option("--rx-buf", "RX buffer size, overrides --buf."):convert(tonumber) + parser:option("--tx-buf", "TX buffer size, overrides --buf."):convert(tonumber) + parser:option("--ipg", "Inter-packet gap, time units (s, ms, us) must be specified."):convert(convertTime) + -- TODO: add delay option + parser:option("--rx-queues", "Number of RX queues per task."):convert(tonumber):default(defaults.rx_queues) + parser:option("--tx-queues", "Number of TX queues per task."):convert(tonumber):default(defaults.tx_queues) +end + +local function configure_main(parser) + parser:description("Task manager used to run any number of various tasks.") + parser:epilog(string.format("Run multiple tasks: \n\t%s [tman options...] [-- [task1 options...]] [-- [task2 options...]] [-- ...]\nGet help on a certain task:\n\t%s -- -h", parser._name, parser._name)) + parser:option("-d --dev", "Devices to transmit from/to."):count("*"):convert(tonumber) +-- passer:option("--dpdk-config", "DPDK config file") + parser:option("-r --rate", "Transmit rate in Mbit/s."):default(10000):convert(tonumber) +end + +local function master(arg0, ...) + memory.testAllocationSpace() + LIBMOON_TASK_NAME = "master" + + multiargs = dashdashSplitArgs(...) + + if not multiargs[1] then + return + end + + libmoon.config.userscript = arg0 -- FIXME: should not be used + libmoon.setupPaths() -- need the userscript first because we want to use the path + libmoon.config.dpdkArgs = {} -- FIXME + + local pargs = {} + local main_defaults = {} + + for i, args in ipairs(multiargs) do + local parser = argparse() + local taskInfo = {} + local defaults = {} + if i == 1 then -- master + parser:name(arg0) + configure_main(parser) + else -- task + local file = args[1] + table.remove(args, 1) + parser:name(file) + parser:option("-n", "Number of examples of task."):default(1):convert(tonumber) + _G_saved = _G + -- run the userscript + local ok = run(file) + if not ok then + return + end + + if _G.configure then + parser:args(unpack(args)) + _G.configure(parser) + end + + defaults = _G.defaults or {} + taskInfo.task = _G.task + taskInfo.file = file + _G.configure = nil + _G.defaults = nil + _G.task = nil + _G = _G_saved + end + configure_common(parser, main_defaults, defaults) + parser:args(unpack(args)) + taskInfo = mergeTables(parser:parse(), taskInfo) + + if i == 1 then + main_defaults.rx_queues = taskInfo.rx_queues + main_defaults.tx_queues = taskInfo.tx_queues + end + + pargs[i-1] = taskInfo + end + + local main_pargs = pargs[0] + pargs[0] = nil + + local rxQueues = 0 + local txQueues = 0 + for i, taskInfo in ipairs(pargs) do + taskInfo.rx_buf = taskInfo.rx_buf or taskInfo.buf or main_pargs.rx_buf or main_pargs.buf + taskInfo.tx_buf = taskInfo.tx_buf or taskInfo.buf or main_pargs.tx_buf or main_pargs.buf + taskInfo.buf = nil + + if not taskInfo.rx_queues or not taskInfo.tx_queues then + log:fatal("Numbers of RX/TX queues for task '%s' must be set. Use --rx-queues, --tx-queues options or set defaults.rx_queues, defaults.tx_queues", taskInfo.file) + end + rxQueues = rxQueues + taskInfo.rx_queues * taskInfo.n + txQueues = txQueues + taskInfo.tx_queues * taskInfo.n + end + if rxQueues == 0 then rxQueues = 1 end + if txQueues == 0 then txQueues = 1 end + + if not libmoon.config.skipInit then + if not dpdk.init() then + log:fatal("Could not initialize DPDK") + end + end + + local taskNum = 0 + for _, dev in ipairs(main_pargs.dev) do + local rxNum = 0 + local txNum = 0 + local dev = device.config{port = dev, txQueues = txQueues, rxQueues = rxQueues, rssQueues = rxQueues} + dev:wait() + + for _, taskInfo in ipairs(pargs) do + libmoon.config.userscript = taskInfo.file -- NB + + for _ = 1, taskInfo.n do + rxInfo = {} + for i = 1, taskInfo.rx_queues do + rxInfo[i] = {queue = dev:getRxQueue(rxNum), bufSize = nil} + rxNum = rxNum + 1 + end + txInfo = {} + for i = 1, taskInfo.tx_queues do + txInfo[i] = {queue = dev:getTxQueue(txNum), bufSize = nil} + txNum = txNum + 1 + end + + _G_saved = _G + -- run the userscript + local ok = run(taskInfo.file) + if not ok then + return + end + mg.startTask("task", taskNum, txInfo, rxInfo, taskInfo) + mg.sleepMillis(100) -- FIXME: this is workaround + _G.configure = nil + _G.defaults = nil + _G.task = nil + _G = _G_saved + + taskNum = taskNum + 1 + end + end + end + mg.waitForTasks() +end +function main(task, exe, ...) + if task == "master" then + if string.find(exe, "/MoonGen$") ~= nil then + libmoon_main(task, exe, ...) + elseif string.find(exe, "/tman$") ~= nil then + master(exe, ...) + else + log:fatal("Unknown executable file name '%s'. Only 'tman' and 'MoonGen' are recognized as valid names. Are we using symlinked or copied file?", exe) + end + else + libmoon_main(task, exe, ...) + end +end diff --git a/lua/pepperfish.lua b/lua/pepperfish.lua new file mode 100644 index 000000000..407f582cb --- /dev/null +++ b/lua/pepperfish.lua @@ -0,0 +1,616 @@ +--[[ + +== Introduction == + + Note that this requires os.clock(), debug.sethook(), + and debug.getinfo() or your equivalent replacements to + be available if this is an embedded application. + + Example usage: + + profiler = newProfiler() + profiler:start() + + < call some functions that take time > + + profiler:stop() + + local outfile = io.open( "profile.txt", "w+" ) + profiler:report( outfile ) + outfile:close() + +== Optionally choosing profiling method == + +The rest of this comment can be ignored if you merely want a good profiler. + + newProfiler(method, sampledelay): + +If method is omitted or "time", will profile based on real performance. +optionally, frequency can be provided to control the number of opcodes +per profiling tick. By default this is 100000, which (on my system) provides +one tick approximately every 2ms and reduces system performance by about 10%. +This can be reduced to increase accuracy at the cost of performance, or +increased for the opposite effect. + +If method is "call", will profile based on function calls. Frequency is +ignored. + + +"time" may bias profiling somewhat towards large areas with "simple opcodes", +as the profiling function (which introduces a certain amount of unavoidable +overhead) will be called more often. This can be minimized by using a larger +sample delay - the default should leave any error largely overshadowed by +statistical noise. With a delay of 1000 I was able to achieve inaccuray of +approximately 25%. Increasing the delay to 100000 left inaccuracy below my +testing error. + +"call" may bias profiling heavily towards areas with many function calls. +Testing found a degenerate case giving a figure inaccurate by approximately +20,000%. (Yes, a multiple of 200.) This is, however, more directly comparable +to common profilers (such as gprof) and also gives accurate function call +counts, which cannot be retrieved from "time". + +I strongly recommend "time" mode, and it is now the default. + +== History == + +2008-09-16 - Time-based profiling and conversion to Lua 5.1 + by Ben Wilhelm ( zorba-pepperfish@pavlovian.net ). + Added the ability to optionally choose profiling methods, along with a new + profiling method. + +Converted to Lua 5, a few improvements, and +additional documentation by Tom Spilman ( tom@sickheadgames.com ) + +Additional corrections and tidying by original author +Daniel Silverstone ( dsilvers@pepperfish.net ) + +== Status == + +Daniel Silverstone is no longer using this code, and judging by how long it's +been waiting for Lua 5.1 support, I don't think Tom Spilman is either. I'm +perfectly willing to take on maintenance, so if you have problems or +questions, go ahead and email me :) +-- Ben Wilhelm ( zorba-pepperfish@pavlovian.net ) ' + +== Copyright == + +Lua profiler - Copyright Pepperfish 2002,2003,2004 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to +do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +--]] + + +-- +-- All profiler related stuff is stored in the top level table '_profiler' +-- +_profiler = {} + + +-- +-- newProfiler() creates a new profiler object for managing +-- the profiler and storing state. Note that only one profiler +-- object can be executing at one time. +-- +function newProfiler(variant, sampledelay) + if _profiler.running then + print("Profiler already running.") + return + end + + variant = variant or "time" + + if variant ~= "time" and variant ~= "call" then + print("Profiler method must be 'time' or 'call'.") + return + end + + local newprof = {} + for k,v in pairs(_profiler) do + newprof[k] = v + end + newprof.variant = variant + newprof.sampledelay = sampledelay or 100000 + return newprof +end + + +-- +-- This function starts the profiler. It will do nothing +-- if this (or any other) profiler is already running. +-- +function _profiler.start(self) + if _profiler.running then + return + end + -- Start the profiler. This begins by setting up internal profiler state + _profiler.running = self + self.rawstats = {} + self.callstack = {} + if self.variant == "time" then + self.lastclock = os.clock() + debug.sethook( _profiler_hook_wrapper_by_time, "", self.sampledelay ) + elseif self.variant == "call" then + debug.sethook( _profiler_hook_wrapper_by_call, "cr" ) + else + print("Profiler method must be 'time' or 'call'.") + sys.exit(1) + end +end + + +-- +-- This function stops the profiler. It will do nothing +-- if a profiler is not running, and nothing if it isn't +-- the currently running profiler. +-- +function _profiler.stop(self) + if _profiler.running ~= self then + return + end + -- Stop the profiler. + debug.sethook( nil ) + _profiler.running = nil +end + + +-- +-- Simple wrapper to handle the hook. You should not +-- be calling this directly. Duplicated to reduce overhead. +-- +function _profiler_hook_wrapper_by_call(action) + if _profiler.running == nil then + debug.sethook( nil ) + end + _profiler.running:_internal_profile_by_call(action) +end +function _profiler_hook_wrapper_by_time(action) + if _profiler.running == nil then + debug.sethook( nil ) + end + _profiler.running:_internal_profile_by_time(action) +end + + +-- +-- This is the main by-function-call function of the profiler and should not +-- be called except by the hook wrapper +-- +function _profiler._internal_profile_by_call(self,action) + -- Since we can obtain the 'function' for the item we've had call us, we + -- can use that... + local caller_info = debug.getinfo( 3 ) + if caller_info == nil then + print "No caller_info" + return + end + + --SHG_LOG("[_profiler._internal_profile] "..(caller_info.name or "")) + + -- Retrieve the most recent activation record... + local latest_ar = nil + if table.getn(self.callstack) > 0 then + latest_ar = self.callstack[table.getn(self.callstack)] + end + + -- Are we allowed to profile this function? + local should_not_profile = 0 + for k,v in pairs(self.prevented_functions) do + if k == caller_info.func then + should_not_profile = v + end + end + -- Also check the top activation record... + if latest_ar then + if latest_ar.should_not_profile == 2 then + should_not_profile = 2 + end + end + + -- Now then, are we in 'call' or 'return' ? + -- print("Profile:", caller_info.name, "SNP:", should_not_profile, + -- "Action:", action ) + if action == "call" then + -- Making a call... + local this_ar = {} + this_ar.should_not_profile = should_not_profile + this_ar.parent_ar = latest_ar + this_ar.anon_child = 0 + this_ar.name_child = 0 + this_ar.children = {} + this_ar.children_time = {} + this_ar.clock_start = os.clock() + -- Last thing to do on a call is to insert this onto the ar stack... + table.insert( self.callstack, this_ar ) + else + local this_ar = latest_ar + if this_ar == nil then + return -- No point in doing anything if no upper activation record + end + + -- Right, calculate the time in this function... + this_ar.clock_end = os.clock() + this_ar.this_time = this_ar.clock_end - this_ar.clock_start + + -- Now, if we have a parent, update its call info... + if this_ar.parent_ar then + this_ar.parent_ar.children[caller_info.func] = + (this_ar.parent_ar.children[caller_info.func] or 0) + 1 + this_ar.parent_ar.children_time[caller_info.func] = + (this_ar.parent_ar.children_time[caller_info.func] or 0 ) + + this_ar.this_time + if caller_info.name == nil then + this_ar.parent_ar.anon_child = + this_ar.parent_ar.anon_child + this_ar.this_time + else + this_ar.parent_ar.name_child = + this_ar.parent_ar.name_child + this_ar.this_time + end + end + -- Now if we're meant to record information about ourselves, do so... + if this_ar.should_not_profile == 0 then + local inforec = self:_get_func_rec(caller_info.func,1) + inforec.count = inforec.count + 1 + inforec.time = inforec.time + this_ar.this_time + inforec.anon_child_time = inforec.anon_child_time + this_ar.anon_child + inforec.name_child_time = inforec.name_child_time + this_ar.name_child + inforec.func_info = caller_info + for k,v in pairs(this_ar.children) do + inforec.children[k] = (inforec.children[k] or 0) + v + inforec.children_time[k] = + (inforec.children_time[k] or 0) + this_ar.children_time[k] + end + end + + -- Last thing to do on return is to drop the last activation record... + table.remove( self.callstack, table.getn( self.callstack ) ) + end +end + + +-- +-- This is the main by-time internal function of the profiler and should not +-- be called except by the hook wrapper +-- +function _profiler._internal_profile_by_time(self,action) + -- we do this first so we add the minimum amount of extra time to this call + local timetaken = os.clock() - self.lastclock + + local depth = 3 + local at_top = true + local last_caller + local caller = debug.getinfo(depth) + while caller do + if not caller.func then caller.func = "(tail call)" end + if self.prevented_functions[caller.func] == nil then + local info = self:_get_func_rec(caller.func, 1, caller) + info.count = info.count + 1 + info.time = info.time + timetaken + if last_caller then + -- we're not the head, so update the "children" times also + if last_caller.name then + info.name_child_time = info.name_child_time + timetaken + else + info.anon_child_time = info.anon_child_time + timetaken + end + info.children[last_caller.func] = + (info.children[last_caller.func] or 0) + 1 + info.children_time[last_caller.func] = + (info.children_time[last_caller.func] or 0) + timetaken + end + end + depth = depth + 1 + last_caller = caller + caller = debug.getinfo(depth) + end + + self.lastclock = os.clock() +end + + +-- +-- This returns a (possibly empty) function record for +-- the specified function. It is for internal profiler use. +-- +function _profiler._get_func_rec(self,func,force,info) + -- Find the function ref for 'func' (if force and not present, create one) + local ret = self.rawstats[func] + if ret == nil and force ~= 1 then + return nil + end + if ret == nil then + -- Build a new function statistics table + ret = {} + ret.func = func + ret.count = 0 + ret.time = 0 + ret.anon_child_time = 0 + ret.name_child_time = 0 + ret.children = {} + ret.children_time = {} + ret.func_info = info + self.rawstats[func] = ret + end + return ret +end + + +-- +-- This writes a profile report to the output file object. If +-- sort_by_total_time is nil or false the output is sorted by +-- the function time minus the time in it's children. +-- +function _profiler.report( self, outfile, sort_by_total_time ) + + outfile:write + [[Lua Profile output created by profiler.lua. Copyright Pepperfish 2002+ + +]] + + -- This is pretty awful. + local terms = {} + if self.variant == "time" then + terms.capitalized = "Sample" + terms.single = "sample" + terms.pastverb = "sampled" + elseif self.variant == "call" then + terms.capitalized = "Call" + terms.single = "call" + terms.pastverb = "called" + else + assert(false) + end + + local total_time = 0 + local ordering = {} + for func,record in pairs(self.rawstats) do + table.insert(ordering, func) + end + + if sort_by_total_time then + table.sort( ordering, + function(a,b) return self.rawstats[a].time > self.rawstats[b].time end + ) + else + table.sort( ordering, + function(a,b) + local arec = self.rawstats[a] + local brec = self.rawstats[b] + local atime = arec.time - (arec.anon_child_time + arec.name_child_time) + local btime = brec.time - (brec.anon_child_time + brec.name_child_time) + return atime > btime + end + ) + end + + for i=1,table.getn(ordering) do + local func = ordering[i] + local record = self.rawstats[func] + local thisfuncname = " " .. self:_pretty_name(func) .. " " + if string.len( thisfuncname ) < 42 then + thisfuncname = + string.rep( "-", (42 - string.len(thisfuncname))/2 ) .. thisfuncname + thisfuncname = + thisfuncname .. string.rep( "-", 42 - string.len(thisfuncname) ) + end + + total_time = total_time + ( record.time - ( record.anon_child_time + + record.name_child_time ) ) + outfile:write( string.rep( "-", 19 ) .. thisfuncname .. + string.rep( "-", 19 ) .. "\n" ) + outfile:write( terms.capitalized.." count: " .. + string.format( "%4d", record.count ) .. "\n" ) + outfile:write( "Time spend total: " .. + string.format( "%4.3f", record.time ) .. "s\n" ) + outfile:write( "Time spent in children: " .. + string.format("%4.3f",record.anon_child_time+record.name_child_time) .. + "s\n" ) + local timeinself = + record.time - (record.anon_child_time + record.name_child_time) + outfile:write( "Time spent in self: " .. + string.format("%4.3f", timeinself) .. "s\n" ) + outfile:write( "Time spent per " .. terms.single .. ": " .. + string.format("%4.5f", record.time/record.count) .. + "s/" .. terms.single .. "\n" ) + outfile:write( "Time spent in self per "..terms.single..": " .. + string.format( "%4.5f", timeinself/record.count ) .. "s/" .. + terms.single.."\n" ) + + -- Report on each child in the form + -- Child called n times and took a.bs + local added_blank = 0 + for k,v in pairs(record.children) do + if self.prevented_functions[k] == nil or + self.prevented_functions[k] == 0 + then + if added_blank == 0 then + outfile:write( "\n" ) -- extra separation line + added_blank = 1 + end + outfile:write( "Child " .. self:_pretty_name(k) .. + string.rep( " ", 41-string.len(self:_pretty_name(k)) ) .. " " .. + terms.pastverb.." " .. string.format("%6d", v) ) + outfile:write( " times. Took " .. + string.format("%4.2f", record.children_time[k] ) .. "s\n" ) + end + end + + outfile:write( "\n" ) -- extra separation line + outfile:flush() + end + outfile:write( "\n\n" ) + outfile:write( "Total time spent in profiled functions: " .. + string.format("%5.3g",total_time) .. "s\n" ) + outfile:write( [[ + +END +]] ) + outfile:flush() +end + + +-- +-- This writes the profile to the output file object as +-- loadable Lua source. +-- +function _profiler.lua_report(self,outfile) + -- Purpose: Write out the entire raw state in a cross-referenceable form. + local ordering = {} + local functonum = {} + for func,record in pairs(self.rawstats) do + table.insert(ordering, func) + functonum[func] = table.getn(ordering) + end + + outfile:write( + "-- Profile generated by profiler.lua Copyright Pepperfish 2002+\n\n" ) + outfile:write( "-- Function names\nfuncnames = {}\n" ) + for i=1,table.getn(ordering) do + local thisfunc = ordering[i] + outfile:write( "funcnames[" .. i .. "] = " .. + string.format("%q", self:_pretty_name(thisfunc)) .. "\n" ) + end + outfile:write( "\n" ) + outfile:write( "-- Function times\nfunctimes = {}\n" ) + for i=1,table.getn(ordering) do + local thisfunc = ordering[i] + local record = self.rawstats[thisfunc] + outfile:write( "functimes[" .. i .. "] = { " ) + outfile:write( "tot=" .. record.time .. ", " ) + outfile:write( "achild=" .. record.anon_child_time .. ", " ) + outfile:write( "nchild=" .. record.name_child_time .. ", " ) + outfile:write( "count=" .. record.count .. " }\n" ) + end + outfile:write( "\n" ) + outfile:write( "-- Child links\nchildren = {}\n" ) + for i=1,table.getn(ordering) do + local thisfunc = ordering[i] + local record = self.rawstats[thisfunc] + outfile:write( "children[" .. i .. "] = { " ) + for k,v in pairs(record.children) do + if functonum[k] then -- non-recorded functions will be ignored now + outfile:write( functonum[k] .. ", " ) + end + end + outfile:write( "}\n" ) + end + outfile:write( "\n" ) + outfile:write( "-- Child call counts\nchildcounts = {}\n" ) + for i=1,table.getn(ordering) do + local thisfunc = ordering[i] + local record = self.rawstats[thisfunc] + outfile:write( "children[" .. i .. "] = { " ) + for k,v in record.children do + if functonum[k] then -- non-recorded functions will be ignored now + outfile:write( v .. ", " ) + end + end + outfile:write( "}\n" ) + end + outfile:write( "\n" ) + outfile:write( "-- Child call time\nchildtimes = {}\n" ) + for i=1,table.getn(ordering) do + local thisfunc = ordering[i] + local record = self.rawstats[thisfunc]; + outfile:write( "children[" .. i .. "] = { " ) + for k,v in pairs(record.children) do + if functonum[k] then -- non-recorded functions will be ignored now + outfile:write( record.children_time[k] .. ", " ) + end + end + outfile:write( "}\n" ) + end + outfile:write( "\n\n-- That is all.\n\n" ) + outfile:flush() +end + +-- Internal function to calculate a pretty name for the profile output +function _profiler._pretty_name(self,func) + + -- Only the data collected during the actual + -- run seems to be correct.... why? + local info = self.rawstats[ func ].func_info + -- local info = debug.getinfo( func ) + + local name = "" + if info.what == "Lua" then + name = "L:" + end + if info.what == "C" then + name = "C:" + end + if info.what == "main" then + name = " :" + end + + if info.name == nil then + name = name .. "<"..tostring(func) .. ">" + else + name = name .. info.name + end + + if info.source then + name = name .. "@" .. info.source + else + if info.what == "C" then + name = name .. "@?" + else + name = name .. "@" + end + end + name = name .. ":" + if info.what == "C" then + name = name .. "?" + else + name = name .. info.linedefined + end + + return name +end + + +-- +-- This allows you to specify functions which you do +-- not want profiled. Setting level to 1 keeps the +-- function from being profiled. Setting level to 2 +-- keeps both the function and its children from +-- being profiled. +-- +-- BUG: 2 will probably act exactly like 1 in "time" mode. +-- If anyone cares, let me (zorba) know and it can be fixed. +-- +function _profiler.prevent(self, func, level) + self.prevented_functions[func] = (level or 1) +end + + +_profiler.prevented_functions = { + [_profiler.start] = 2, + [_profiler.stop] = 2, + [_profiler._internal_profile_by_time] = 2, + [_profiler._internal_profile_by_call] = 2, + [_profiler_hook_wrapper_by_time] = 2, + [_profiler_hook_wrapper_by_call] = 2, + [_profiler.prevent] = 2, + [_profiler._get_func_rec] = 2, + [_profiler.report] = 2, + [_profiler.lua_report] = 2, + [_profiler._pretty_name] = 2 +}