18
18
package org .apache .spark .ui
19
19
20
20
import java .text .SimpleDateFormat
21
- import java .util .Date
21
+ import java .util .{ Locale , Date }
22
22
23
23
import scala .xml .Node
24
+ import org .apache .spark .Logging
24
25
25
26
/** Utility functions for generating XML pages with spark content. */
26
- private [spark] object UIUtils {
27
+ private [spark] object UIUtils extends Logging {
27
28
28
29
// SimpleDateFormat is not thread-safe. Don't expose it to avoid improper use.
29
30
private val dateFormat = new ThreadLocal [SimpleDateFormat ]() {
@@ -49,6 +50,80 @@ private[spark] object UIUtils {
49
50
" %.1f h" .format(hours)
50
51
}
51
52
53
+ /** Generate a verbose human-readable string representing a duration such as "5 second 35 ms" */
54
+ def formatDurationVerbose (ms : Long ): String = {
55
+ try {
56
+ val second = 1000L
57
+ val minute = 60 * second
58
+ val hour = 60 * minute
59
+ val day = 24 * hour
60
+ val week = 7 * day
61
+ val year = 365 * day
62
+
63
+ def toString (num : Long , unit : String ): String = {
64
+ if (num == 0 ) {
65
+ " "
66
+ } else if (num == 1 ) {
67
+ s " $num $unit"
68
+ } else {
69
+ s " $num ${unit}s "
70
+ }
71
+ }
72
+
73
+ val millisecondsString = if (ms >= second && ms % second == 0 ) " " else s " ${ms % second} ms "
74
+ val secondString = toString((ms % minute) / second, " second" )
75
+ val minuteString = toString((ms % hour) / minute, " minute" )
76
+ val hourString = toString((ms % day) / hour, " hour" )
77
+ val dayString = toString((ms % week) / day, " day" )
78
+ val weekString = toString((ms % year) / week, " week" )
79
+ val yearString = toString(ms / year, " year" )
80
+
81
+ Seq (
82
+ second -> millisecondsString,
83
+ minute -> s " $secondString $millisecondsString" ,
84
+ hour -> s " $minuteString $secondString" ,
85
+ day -> s " $hourString $minuteString $secondString" ,
86
+ week -> s " $dayString $hourString $minuteString" ,
87
+ year -> s " $weekString $dayString $hourString"
88
+ ).foreach { case (durationLimit, durationString) =>
89
+ if (ms < durationLimit) {
90
+ // if time is less than the limit (upto year)
91
+ return durationString
92
+ }
93
+ }
94
+ // if time is more than a year
95
+ return s " $yearString $weekString $dayString"
96
+ } catch {
97
+ case e : Exception =>
98
+ logError(" Error converting time to string" , e)
99
+ // if there is some error, return blank string
100
+ return " "
101
+ }
102
+ }
103
+
104
+ /** Generate a human-readable string representing a number (e.g. 100 K) */
105
+ def formatNumber (records : Double ): String = {
106
+ val trillion = 1e12
107
+ val billion = 1e9
108
+ val million = 1e6
109
+ val thousand = 1e3
110
+
111
+ val (value, unit) = {
112
+ if (records >= 2 * trillion) {
113
+ (records / trillion, " T" )
114
+ } else if (records >= 2 * billion) {
115
+ (records / billion, " B" )
116
+ } else if (records >= 2 * million) {
117
+ (records / million, " M" )
118
+ } else if (records >= 2 * thousand) {
119
+ (records / thousand, " K" )
120
+ } else {
121
+ (records, " " )
122
+ }
123
+ }
124
+ " %.1f%s" .formatLocal(Locale .US , value, unit)
125
+ }
126
+
52
127
// Yarn has to go through a proxy so the base uri is provided and has to be on all links
53
128
val uiRoot : String = Option (System .getenv(" APPLICATION_WEB_PROXY_BASE" )).getOrElse(" " )
54
129
@@ -146,21 +221,36 @@ private[spark] object UIUtils {
146
221
/** Returns an HTML table constructed by generating a row for each object in a sequence. */
147
222
def listingTable [T ](
148
223
headers : Seq [String ],
149
- makeRow : T => Seq [Node ],
150
- rows : Seq [T ],
224
+ generateDataRow : T => Seq [Node ],
225
+ data : Seq [T ],
151
226
fixedWidth : Boolean = false ): Seq [Node ] = {
152
227
153
- val colWidth = 100 .toDouble / headers.size
154
- val colWidthAttr = if (fixedWidth) colWidth + " %" else " "
155
228
var tableClass = " table table-bordered table-striped table-condensed sortable"
156
229
if (fixedWidth) {
157
230
tableClass += " table-fixed"
158
231
}
159
-
232
+ val colWidth = 100 .toDouble / headers.size
233
+ val colWidthAttr = if (fixedWidth) colWidth + " %" else " "
234
+ val headerRow : Seq [Node ] = {
235
+ // if none of the headers have "\n" in them
236
+ if (headers.forall(! _.contains(" \n " ))) {
237
+ // represent header as simple text
238
+ headers.map(h => <th width ={colWidthAttr}>{h}</th >)
239
+ } else {
240
+ // represent header text as list while respecting "\n"
241
+ headers.map { case h =>
242
+ <th width ={colWidthAttr}>
243
+ <ul class =" unstyled" >
244
+ { h.split(" \n " ).map { case t => <li > {t} </li > } }
245
+ </ul >
246
+ </th >
247
+ }
248
+ }
249
+ }
160
250
<table class ={tableClass}>
161
- <thead >{headers.map(h => < th width ={colWidthAttr}>{h}</ th >) }</thead >
251
+ <thead >{headerRow }</thead >
162
252
<tbody >
163
- {rows .map(r => makeRow (r))}
253
+ {data .map(r => generateDataRow (r))}
164
254
</tbody >
165
255
</table >
166
256
}
0 commit comments