Skip to content
This repository was archived by the owner on Mar 11, 2019. It is now read-only.

Commit fa407ed

Browse files
committed
Merge pull request #74 from Spirals-Team/feature/docker
feature(docker): implement energy monitoring of a container
2 parents 25322e1 + a441a56 commit fa407ed

File tree

14 files changed

+182
-114
lines changed

14 files changed

+182
-114
lines changed

powerapi-cli/src/main/scala/org/powerapi/app/PowerAPI.scala

+44-67
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,20 @@
2323
package org.powerapi.app
2424

2525
import java.lang.management.ManagementFactory
26-
import org.powerapi.core.target.{Application, All, Process, Target}
26+
import org.powerapi.core.target._
2727
import org.powerapi.module.rapl.RAPLModule
2828
import org.powerapi.reporter.{FileDisplay, JFreeChartDisplay, ConsoleDisplay}
29-
import org.powerapi.{PowerMonitoring, PowerMeter}
29+
import org.powerapi.{PowerDisplay, PowerMonitoring, PowerMeter}
3030
import org.powerapi.core.power._
3131
import org.powerapi.module.cpu.dvfs.CpuDvfsModule
3232
import org.powerapi.module.cpu.simple.{SigarCpuSimpleModule, ProcFSCpuSimpleModule}
33-
import org.powerapi.module.libpfm.{LibpfmModule, LibpfmHelper, LibpfmCoreProcessModule, LibpfmCoreModule, LibpfmProcessModule}
33+
import org.powerapi.module.libpfm._
3434
import org.powerapi.module.extPMeter.powerspy.PowerSpyModule
3535
import org.powerapi.module.extPMeter.g5k.G5kOmegaWattModule
3636
import scala.concurrent.duration.DurationInt
3737
import scala.sys
3838
import scala.sys.process.stringSeqToProcess
39+
import scala.util.matching.Regex
3940

4041
/**
4142
* PowerAPI CLI.
@@ -47,8 +48,9 @@ object PowerAPI extends App {
4748
val modulesR = """(procfs-cpu-simple|sigar-cpu-simple|cpu-dvfs|libpfm|libpfm-process|libpfm-core|libpfm-core-process|powerspy|g5k-omegawatt|rapl)(,(procfs-cpu-simple|sigar-cpu-simple|cpu-dvfs|libpfm|libpfm-process|libpfm-core|libpfm-core-process|powerspy|g5k-omegawatt|rapl))*""".r
4849
val aggR = """max|min|geomean|logsum|mean|median|stdev|sum|variance""".r
4950
val durationR = """\d+""".r
50-
val pidR = """(\d+)""".r
51-
val appR = """(.+)""".r
51+
val pidsR = """(\d+)(,(\d+))*""".r
52+
val appsR = """([^,]+)(,([^,]+))*""".r
53+
val containersR = """([^,]+)(,([^,]+))*""".r
5254

5355
@volatile var powerMeters = Seq[PowerMeter]()
5456
@volatile var monitors = Seq[PowerMonitoring]()
@@ -60,13 +62,8 @@ object PowerAPI extends App {
6062
powerMeters = Seq()
6163
}
6264

63-
def validateModules(str: String) = str match {
64-
case modulesR(_*) => true
65-
case _ => false
66-
}
67-
68-
def validateAgg(str: String): Boolean = str match {
69-
case aggR(_*) => true
65+
def validate(regex: Regex, str: String) = str match {
66+
case regex(_*) => true
7067
case _ => false
7168
}
7269

@@ -84,42 +81,25 @@ object PowerAPI extends App {
8481
}
8582
}
8683

87-
def validateDuration(str: String): Boolean = str match {
88-
case durationR(_*) => true
89-
case _ => false
90-
}
91-
92-
implicit def targetsStrToTargets(str: String): Seq[Target] = {
93-
val strTargets = if(str.split(",").contains("all")) {
94-
"all"
95-
}
96-
else str
97-
98-
(for(target <- strTargets.split(",")) yield {
99-
target match {
100-
case "" => Process(ManagementFactory.getRuntimeMXBean.getName.split("@")(0).toInt)
101-
case "all" => All
102-
case pidR(pid) => Process(pid.toInt)
103-
case appR(app) => Application(app)
104-
}
105-
}).toSeq
106-
}
107-
10884
def printHelp(): Unit = {
10985
val str =
11086
"""
11187
|PowerAPI, Spirals Team
11288
|
11389
|Build a software-defined power meter. Do not forget to configure correctly the modules.
114-
|You can use different settings per software-defined power meter for some modules by using the optional prefix option.
90+
|Different settings can be used per software-defined power meter by using the prefix option.
11591
|Please, refer to the documentation inside the GitHub wiki for further details.
11692
|
117-
|usage: ./powerapi modules [procfs-cpu-simple|sigar-cpu-simple|cpu-dvfs|libpfm|libpfm-process|libpfm-core|libpfm-core-proces|powerspy|g5k-omegawatt|rapl,...] *--prefix [name]* \
118-
| monitor --frequency [ms] --targets [pid, ..., app, ...|all] --agg [max|min|geomean|logsum|mean|median|stdev|sum|variance] --[console,file [filepath],chart] \
93+
|usage: ./powerapi modules procfs-cpu-simple|sigar-cpu-simple|cpu-dvfs|libpfm|libpfm-process|libpfm-core|libpfm-core-process|powerspy|g5k-omegawatt|rapl (1, *) *--prefix [name]*
94+
| monitor (1, *)
95+
| --frequency $MILLISECONDS
96+
| --self (0, 1) --pids [pid, ...] (0, *) --apps [app, ...] (0, *) --containers [id, ...] (0, *) | all (0, 1)
97+
| --agg max|min|geomean|logsum|mean|median|stdev|sum|variance
98+
| --console (0, 1) --file $FILEPATH (0, *) --chart (0, 1)
11999
| duration [s]
120100
|
121-
|example: ./powerapi modules procfs-cpu-simple monitor --frequency 1000 --targets firefox,chrome --agg max --console \
122-
| modules powerspy --prefix powermeter2 monitor --frequency 1000 --targets all --agg max --console \
101+
|example: ./powerapi modules procfs-cpu-simple monitor --frequency 1000 --apps firefox,chrome --agg max --console \
102+
| modules powerspy --prefix powermeter2 monitor --frequency 1000 --all --agg max --console \
123103
| duration 30
124104
""".stripMargin
125105

@@ -128,30 +108,34 @@ object PowerAPI extends App {
128108

129109
def cli(options: List[Map[Symbol, Any]], duration: String, args: List[String]): (List[Map[Symbol, Any]], String) = args match {
130110
case Nil => (options, duration)
131-
case "modules" :: value :: "--prefix" :: prefix :: "monitor" :: tail if validateModules(value) => {
111+
case "modules" :: value :: "--prefix" :: prefix :: "monitor" :: tail if validate(modulesR, value) => {
132112
val (remainingArgs, monitors) = cliMonitorsSubcommand(List(), Map(), tail.map(_.toString))
133113
cli(options :+ Map('modules -> value, 'prefix -> Some(prefix), 'monitors -> monitors), duration, remainingArgs)
134114
}
135-
case "modules" :: value :: "monitor" :: tail if validateModules(value) => {
115+
case "modules" :: value :: "monitor" :: tail if validate(modulesR, value) => {
136116
val (remainingArgs, monitors) = cliMonitorsSubcommand(List(), Map(), tail.map(_.toString))
137117
cli(options :+ Map('modules -> value, 'prefix -> None, 'monitors -> monitors), duration, remainingArgs)
138118
}
139-
case "duration" :: value :: tail if validateDuration(value) => cli(options, value, tail)
119+
case "duration" :: value :: tail if validate(durationR, value) => cli(options, value, tail)
140120
case option :: tail => println(s"unknown cli option $option"); sys.exit(1)
141121
}
142122

143123
def cliMonitorsSubcommand(options: List[Map[Symbol, Any]], currentMonitor: Map[Symbol, Any], args: List[String]): (List[String], List[Map[Symbol, Any]]) = args match {
144124
case Nil => (List(), options :+ currentMonitor)
145-
case "modules" :: value :: "--prefix" :: prefix :: "monitor" :: tail if validateModules(value) => (List("modules", value, "--prefix", prefix, "monitor") ++ tail, options :+ currentMonitor)
146-
case "modules" :: value :: "monitor" :: tail if validateModules(value) => (List("modules", value, "monitor") ++ tail, options :+ currentMonitor)
147-
case "duration" :: value :: tail if validateDuration(value) => (List("duration", value) ++ tail, options :+ currentMonitor)
125+
case "modules" :: value :: "--prefix" :: prefix :: "monitor" :: tail if validate(modulesR, value) => (List("modules", value, "--prefix", prefix, "monitor") ++ tail, options :+ currentMonitor)
126+
case "modules" :: value :: "monitor" :: tail if validate(modulesR, value) => (List("modules", value, "monitor") ++ tail, options :+ currentMonitor)
127+
case "duration" :: value :: tail if validate(durationR, value) => (List("duration", value) ++ tail, options :+ currentMonitor)
148128
case "monitor" :: tail => cliMonitorsSubcommand(options :+ currentMonitor, Map(), tail)
149-
case "--frequency" :: value :: tail if validateDuration(value) => cliMonitorsSubcommand(options, currentMonitor ++ Map('frequency -> value), tail)
150-
case "--targets" :: value :: tail => cliMonitorsSubcommand(options, currentMonitor ++ Map('targets -> value), tail)
151-
case "--agg" :: value :: tail if validateAgg(value) => cliMonitorsSubcommand(options, currentMonitor ++ Map('agg -> value), tail)
152-
case "--console" :: tail => cliMonitorsSubcommand(options, currentMonitor ++ Map('console -> "true"), tail)
153-
case "--file" :: value :: tail => cliMonitorsSubcommand(options, currentMonitor ++ Map('file -> value), tail)
154-
case "--chart" :: tail => cliMonitorsSubcommand(options, currentMonitor ++ Map('chart -> "true"), tail)
129+
case "--frequency" :: value :: tail if validate(durationR, value) => cliMonitorsSubcommand(options, currentMonitor ++ Map('frequency -> value), tail)
130+
case "--self" :: tail => cliMonitorsSubcommand(options, currentMonitor + ('targets -> (currentMonitor.getOrElse('targets, Set[Any]()).asInstanceOf[Set[Any]] + Process(ManagementFactory.getRuntimeMXBean.getName.split("@")(0).toInt))), tail)
131+
case "--pids" :: value :: tail if validate(pidsR, value) => cliMonitorsSubcommand(options, currentMonitor + ('targets -> (currentMonitor.getOrElse('targets, Set[Any]()).asInstanceOf[Set[Any]] ++ value.split(",").map(pid => Process(pid.toInt)))), tail)
132+
case "--apps" :: value :: tail if validate(appsR, value) => cliMonitorsSubcommand(options, currentMonitor + ('targets -> (currentMonitor.getOrElse('targets, Set[Any]()).asInstanceOf[Set[Any]] ++ value.split(",").map(app => Application(app)))), tail)
133+
case "--containers" :: value :: tail if validate(containersR, value) => cliMonitorsSubcommand(options, currentMonitor + ('targets -> (currentMonitor.getOrElse('targets, Set[Any]()).asInstanceOf[Set[Any]] ++ value.split(",").map(container => Container(container)))), tail)
134+
case "--all" :: tail => cliMonitorsSubcommand(options, currentMonitor + ('targets -> (currentMonitor.getOrElse('targets, Set[Any]()).asInstanceOf[Set[Any]] + All)), tail)
135+
case "--agg" :: value :: tail if validate(aggR, value) => cliMonitorsSubcommand(options, currentMonitor ++ Map('agg -> value), tail)
136+
case "--console" :: tail => cliMonitorsSubcommand(options, currentMonitor + ('displays -> (currentMonitor.getOrElse('displays, Set[Any]()).asInstanceOf[Set[Any]] + new ConsoleDisplay)), tail)
137+
case "--file" :: value :: tail => cliMonitorsSubcommand(options, currentMonitor + ('displays -> (currentMonitor.getOrElse('displays, Set[Any]()).asInstanceOf[Set[Any]] + new FileDisplay(value))), tail)
138+
case "--chart" :: tail => cliMonitorsSubcommand(options, currentMonitor + ('displays -> (currentMonitor.getOrElse('displays, Set[Any]()).asInstanceOf[Set[Any]] + new JFreeChartDisplay)), tail)
155139
case option :: tail => println(s"unknown monitor option $option"); sys.exit(1)
156140
}
157141

@@ -162,6 +146,8 @@ object PowerAPI extends App {
162146

163147
else {
164148
if(System.getProperty("os.name").toLowerCase.indexOf("nix") >= 0 || System.getProperty("os.name").toLowerCase.indexOf("nux") >= 0) Seq("bash", "scripts/system.bash").!
149+
System.setProperty("java.library.path", "lib")
150+
165151
val (configuration, duration) = cli(List(), "3600", args.toList)
166152

167153
var libpfmHelper: Option[LibpfmHelper] = None
@@ -192,28 +178,18 @@ object PowerAPI extends App {
192178

193179
for(monitorConf <- powerMeterConf('monitors).asInstanceOf[List[Map[Symbol, Any]]]) {
194180
val frequency = monitorConf.getOrElse('frequency, "1000").toString.toInt.milliseconds
195-
val targets: Seq[Target] = monitorConf.getOrElse('targets, "").toString.toLowerCase
181+
val targets = {
182+
val uniqueTargets = monitorConf.getOrElse('targets, Set(Process(ManagementFactory.getRuntimeMXBean.getName.split("@")(0).toInt))).asInstanceOf[Set[Target]].toSeq
183+
if(uniqueTargets.contains(All)) Seq(All) else uniqueTargets
184+
}
196185
val agg: Seq[Power] => Power = aggStrToAggFunction(monitorConf.getOrElse('agg, "max").toString.toLowerCase)
197-
val console = monitorConf.getOrElse('console, "").toString
198-
val file = monitorConf.getOrElse('file, "").toString
199-
val chart = monitorConf.getOrElse('chart, "").toString
186+
val displays = monitorConf.getOrElse('displays, Set(new ConsoleDisplay)).asInstanceOf[Set[PowerDisplay]]
200187

201188
val monitor = powerMeter.monitor(frequency)(targets: _*)(agg)
202189
monitors :+= monitor
203190

204-
if(console != "") {
205-
val consoleDisplay = new ConsoleDisplay()
206-
monitor.to(consoleDisplay)
207-
}
208-
209-
if(file != "") {
210-
val fileDisplay = new FileDisplay(file)
211-
monitor.to(fileDisplay)
212-
}
213-
214-
if(chart != "") {
215-
val chartDisplay = new JFreeChartDisplay()
216-
monitor.to(chartDisplay)
191+
for(display <- displays) {
192+
monitor.to(display)
217193
}
218194
}
219195
}
@@ -231,3 +207,4 @@ object PowerAPI extends App {
231207
shutdownHookThread.remove()
232208
sys.exit(0)
233209
}
210+

powerapi-core/build.sbt

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ name := "powerapi-core"
33
organization := "org.powerapi"
44

55
resolvers ++= Seq(
6-
"JBoss Thirdparty Uploads" at "https://repository.jboss.org/nexus/content/repositories/thirdparty-uploads/"
6+
"JBoss Thirdparty Uploads" at "https://repository.jboss.org/nexus/content/repositories/thirdparty-uploads/",
7+
"softprops-maven" at "http://dl.bintray.com/content/softprops/maven"
78
)
89

910
// App
@@ -22,7 +23,8 @@ libraryDependencies ++= Seq(
2223
"io.spray" %% "spray-routing" % "1.3.3",
2324
"io.spray" %% "spray-json" % "1.3.2",
2425
"com.github.nscala-time" %% "nscala-time" % "2.0.0",
25-
"net.java.dev.jna" % "jna" % "4.1.0"
26+
"net.java.dev.jna" % "jna" % "4.1.0",
27+
"com.github.docker-java" % "docker-java" % "2.1.1"
2628
)
2729

2830
// Tests

powerapi-core/src/main/scala/org/powerapi/core/OSHelper.scala

+37-1
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ import org.apache.logging.log4j.LogManager
2929
import org.hyperic.sigar.{Sigar, SigarException, SigarProxyCache}
3030
import org.hyperic.sigar.ptql.ProcessFinder
3131
import org.powerapi.core.FileHelper.using
32-
import org.powerapi.core.target.{All, Application, Process, Target, TargetUsageRatio}
32+
import org.powerapi.core.target.{All, Application, Container, Process, Target, TargetUsageRatio}
3333
import org.powerapi.module.{Cache, CacheKey}
3434
import scala.collection.JavaConversions._
3535
import scala.sys.process.stringSeqToProcess
36+
import com.github.dockerjava.core.DockerClientBuilder
3637

3738
/**
3839
* This is not a monitoring target. It's an internal wrapper for the Thread IDentifier.
@@ -78,6 +79,13 @@ trait OSHelper {
7879
* @param application: targeted application.
7980
*/
8081
def getProcesses(application: Application): Set[Process]
82+
83+
/**
84+
* Get the list of processes running on a container.
85+
*
86+
* @param container: targeted container.
87+
*/
88+
def getProcesses(container: Container): Set[Process] = throw new Exception("The container handling is not available on this operating system.")
8189

8290
/**
8391
* Get the list of thread behind a Process.
@@ -131,6 +139,16 @@ trait OSHelper {
131139
}
132140
}
133141
)
142+
case container: Container => Some(
143+
getProcesses(container).foldLeft(0: Long) {
144+
(acc, process: Process) => {
145+
getProcessCpuTime(process) match {
146+
case Some(value) => acc + value
147+
case _ => acc
148+
}
149+
}
150+
}
151+
)
134152
case _ => None
135153
}
136154
}
@@ -151,6 +169,16 @@ trait OSHelper {
151169
}
152170
}
153171
)
172+
case container: Container => TargetUsageRatio(
173+
getProcesses(container).foldLeft(0.0: Double) {
174+
(acc, process: Process) => {
175+
getProcessCpuPercent(muid, process) match {
176+
case TargetUsageRatio(value) => acc + value
177+
case _ => acc
178+
}
179+
}
180+
}
181+
)
154182
case _ => TargetUsageRatio(0.0)
155183
}
156184
}
@@ -168,6 +196,8 @@ class LinuxHelper extends Configuration(None) with OSHelper {
168196
private val GlobalStatFormat = """cpu\s+([\d\s]+)""".r
169197
private val TimeInStateFormat = """(\d+)\s+(\d+)""".r
170198

199+
val docker = DockerClientBuilder.getInstance("unix:///var/run/docker.sock").build()
200+
171201
/**
172202
* This file allows to get all the cpu frequencies with the help of procfs and cpufreq_utils.
173203
*/
@@ -242,6 +272,12 @@ class LinuxHelper extends Configuration(None) with OSHelper {
242272
case PSFormat(pid) => Process(pid.toInt)
243273
}.toSet
244274
}
275+
276+
override def getProcesses(container: Container): Set[Process] = {
277+
docker.topContainerCmd(container.id).withPsArgs("-Aopid").exec.getProcesses.flatten.map(
278+
process => Process(process.toInt)
279+
).toSet
280+
}
245281

246282
def getThreads(process: Process): Set[Thread] = {
247283
val pidDirectory = new File(taskPath.replace("%?pid", s"${process.pid}"))

powerapi-core/src/main/scala/org/powerapi/core/target/Target.scala

+13-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ trait Target
3939
* @author <a href="mailto:[email protected]">Maxime Colmant</a>
4040
*/
4141
case class Process(pid: Int) extends Target {
42-
override def toString(): String = s"$pid"
42+
override def toString: String = s"$pid"
4343
}
4444

4545
/**
@@ -51,7 +51,18 @@ case class Process(pid: Int) extends Target {
5151
* @author <a href="mailto:[email protected]">Maxime Colmant</a>
5252
*/
5353
case class Application(name: String) extends Target {
54-
override def toString(): String = name
54+
override def toString: String = name
55+
}
56+
57+
/**
58+
* Monitoring targets for a specific container.
59+
*
60+
* @param name: id of the container.
61+
*
62+
* @author <a href="mailto:[email protected]">Loïc Huertas</a>
63+
*/
64+
case class Container(id: String) extends Target {
65+
override def toString: String = id
5566
}
5667

5768
/**

powerapi-core/src/main/scala/org/powerapi/module/cpu/simple/CpuSensor.scala

+4-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ package org.powerapi.module.cpu.simple
2525
import org.powerapi.core.{MessageBus, OSHelper}
2626
import org.powerapi.module.SensorComponent
2727
import org.powerapi.core.MonitorChannel.MonitorTick
28-
import org.powerapi.core.target.{All, Application, Process, TargetUsageRatio}
28+
import org.powerapi.core.target.{All, Application, Container, Process, TargetUsageRatio}
2929
import org.powerapi.module.cpu.UsageMetricsChannel.publishUsageReport
3030
import org.powerapi.module.SensorChannel.{MonitorStop, MonitorStopAll}
3131
import scala.reflect.ClassTag
@@ -44,12 +44,14 @@ class CpuSensor(eventBus: MessageBus, osHelper: OSHelper) extends SensorComponen
4444
def targetCpuUsageRatio(monitorTick: MonitorTick): TargetUsageRatio = {
4545
val processClaz = implicitly[ClassTag[Process]].runtimeClass
4646
val appClaz = implicitly[ClassTag[Application]].runtimeClass
47+
val containerClaz = implicitly[ClassTag[Container]].runtimeClass
4748

4849
monitorTick.target match {
49-
case target if processClaz.isInstance(target) || appClaz.isInstance(target) => {
50+
case target if processClaz.isInstance(target) || appClaz.isInstance(target) || containerClaz.isInstance(target) => {
5051
osHelper.getTargetCpuPercent(monitorTick.muid, target)
5152
}
5253
case All => osHelper.getGlobalCpuPercent(monitorTick.muid)
54+
case _ => log.warning("Only Process, Application, Container or All targets can be used with this Sensor"); TargetUsageRatio(0.0)
5355
}
5456
}
5557

powerapi-core/src/main/scala/org/powerapi/module/extPMeter/ExtPMeterFormula.scala

+4-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import akka.event.LoggingReceive
2626
import org.powerapi.core.{OSHelper, APIComponent, MessageBus}
2727
import org.powerapi.core.MonitorChannel.{MonitorTick, subscribeMonitorTick}
2828
import org.powerapi.core.power._
29-
import org.powerapi.core.target.{Application, All, Process, TargetUsageRatio}
29+
import org.powerapi.core.target.{Application, All, Container, Process, TargetUsageRatio}
3030
import org.powerapi.module.{Cache, CacheKey}
3131
import org.powerapi.module.PowerChannel.publishRawPowerReport
3232
import org.powerapi.module.extPMeter.ExtPMeterChannel.{ExtPMeterPower, subscribePMeterPower}
@@ -66,12 +66,13 @@ class ExtPMeterFormula(eventBus: MessageBus, osHelper: OSHelper, idlePower: Powe
6666

6767
val processClaz = implicitly[ClassTag[Process]].runtimeClass
6868
val appClaz = implicitly[ClassTag[Application]].runtimeClass
69+
val containerClaz = implicitly[ClassTag[Container]].runtimeClass
6970

7071
lazy val now = monitorTick.target match {
71-
case target if processClaz.isInstance(target) || appClaz.isInstance(target) => {
72+
case target if processClaz.isInstance(target) || appClaz.isInstance(target) || containerClaz.isInstance(target) => {
7273
lazy val targetCpuTime = osHelper.getTargetCpuTime(target) match {
7374
case Some(time) => time
74-
case _ => 0l
75+
case _ => log.warning("Only Process, Application, Container or All targets can be used with this Sensor"); 0l
7576
}
7677

7778
(targetCpuTime, activeCpuTime)

0 commit comments

Comments
 (0)