1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2  *
3  * Copyright (C) 2009-2012 Richard Hughes <richard@hughsie.com>
4  *
5  * Licensed under the GNU General Public License Version 2
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21 
22 #ifdef HAVE_CONFIG_H
23 #  include <config.h>
24 #endif
25 
26 #include <glib.h>
27 #include <signal.h>
28 #include <gio/gio.h>
29 
30 #include "cd-state.h"
31 
32 #define CD_STATE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), CD_TYPE_STATE, CdStatePrivate))
33 
34 struct _CdStatePrivate
35 {
36 	gboolean		 enable_profile;
37 	gboolean		 process_event_sources;
38 	gchar			*id;
39 	gdouble			 global_share;
40 	gdouble			*step_profile;
41 	GTimer			*timer;
42 	guint			 current;
43 	guint			 last_percentage;
44 	guint			*step_data;
45 	guint			 steps;
46 	gulong			 percentage_child_id;
47 	gulong			 subpercentage_child_id;
48 	CdState			*child;
49 	CdState			*parent;
50 };
51 
52 enum {
53 	SIGNAL_PERCENTAGE_CHANGED,
54 	SIGNAL_SUBPERCENTAGE_CHANGED,
55 	SIGNAL_LAST
56 };
57 
58 enum {
59 	PROP_0,
60 	PROP_SPEED,
61 	PROP_LAST
62 };
63 
64 static guint signals [SIGNAL_LAST] = { 0 };
65 
G_DEFINE_TYPE(CdState,cd_state,G_TYPE_OBJECT)66 G_DEFINE_TYPE (CdState, cd_state, G_TYPE_OBJECT)
67 
68 #define CD_STATE_SPEED_SMOOTHING_ITEMS		5
69 
70 /**
71  * cd_state_error_quark:
72  **/
73 GQuark
74 cd_state_error_quark (void)
75 {
76 	static GQuark quark = 0;
77 	if (!quark)
78 		quark = g_quark_from_static_string ("cd_state_error");
79 	return quark;
80 }
81 
82 /**
83  * cd_state_set_enable_profile:
84  **/
85 void
cd_state_set_enable_profile(CdState * state,gboolean enable_profile)86 cd_state_set_enable_profile (CdState *state, gboolean enable_profile)
87 {
88 	g_return_if_fail (CD_IS_STATE (state));
89 	state->priv->enable_profile = enable_profile;
90 }
91 
92 /**
93  * cd_state_discrete_to_percent:
94  **/
95 static gfloat
cd_state_discrete_to_percent(guint discrete,guint steps)96 cd_state_discrete_to_percent (guint discrete, guint steps)
97 {
98 	/* check we are in range */
99 	if (discrete > steps)
100 		return 100;
101 	if (steps == 0) {
102 		g_warning ("steps is 0!");
103 		return 0;
104 	}
105 	return ((gfloat) discrete * (100.0f / (gfloat) (steps)));
106 }
107 
108 /**
109  * cd_state_print_parent_chain:
110  **/
111 static void
cd_state_print_parent_chain(CdState * state,guint level)112 cd_state_print_parent_chain (CdState *state, guint level)
113 {
114 	if (state->priv->parent != NULL)
115 		cd_state_print_parent_chain (state->priv->parent, level + 1);
116 	g_print ("%u) %s (%u/%u)\n",
117 		 level, state->priv->id, state->priv->current, state->priv->steps);
118 }
119 
120 /**
121  * cd_state_set_percentage:
122  **/
123 gboolean
cd_state_set_percentage(CdState * state,guint percentage)124 cd_state_set_percentage (CdState *state, guint percentage)
125 {
126 	/* is it the same */
127 	if (percentage == state->priv->last_percentage)
128 		return FALSE;
129 
130 	/* is it invalid */
131 	if (percentage > 100) {
132 		cd_state_print_parent_chain (state, 0);
133 		g_warning ("percentage %u%% is invalid on %p!",
134 			   percentage, state);
135 		return FALSE;
136 	}
137 
138 	/* is it less */
139 	if (percentage < state->priv->last_percentage) {
140 		if (state->priv->enable_profile) {
141 			cd_state_print_parent_chain (state, 0);
142 			g_critical ("percentage should not go down from %u to %u on %p!",
143 				    state->priv->last_percentage, percentage, state);
144 		}
145 		return FALSE;
146 	}
147 
148 	/* save */
149 	state->priv->last_percentage = percentage;
150 
151 	/* are we so low we don't care */
152 	if (state->priv->global_share < 0.001)
153 		return FALSE;
154 
155 	/* emit */
156 	g_signal_emit (state, signals [SIGNAL_PERCENTAGE_CHANGED], 0, percentage);
157 	return TRUE;
158 }
159 
160 /**
161  * cd_state_get_percentage:
162  **/
163 guint
cd_state_get_percentage(CdState * state)164 cd_state_get_percentage (CdState *state)
165 {
166 	return state->priv->last_percentage;
167 }
168 
169 /**
170  * cd_state_set_subpercentage:
171  **/
172 static gboolean
cd_state_set_subpercentage(CdState * state,guint percentage)173 cd_state_set_subpercentage (CdState *state, guint percentage)
174 {
175 	/* are we so low we don't care */
176 	if (state->priv->global_share < 0.01)
177 		return TRUE;
178 
179 	/* just emit */
180 	g_signal_emit (state, signals [SIGNAL_SUBPERCENTAGE_CHANGED], 0, percentage);
181 	return TRUE;
182 }
183 
184 /**
185  * cd_state_child_percentage_changed_cb:
186  **/
187 static void
cd_state_child_percentage_changed_cb(CdState * child,guint percentage,CdState * state)188 cd_state_child_percentage_changed_cb (CdState *child, guint percentage, CdState *state)
189 {
190 	gfloat offset;
191 	gfloat range;
192 	gfloat extra;
193 	guint parent_percentage;
194 
195 	/* propagate up the stack if CdState has only one step */
196 	if (state->priv->steps == 1) {
197 		cd_state_set_percentage (state, percentage);
198 		return;
199 	}
200 
201 	/* did we call done on a state that did not have a size set? */
202 	if (state->priv->steps == 0)
203 		return;
204 
205 	/* always provide two levels of signals */
206 	cd_state_set_subpercentage (state, percentage);
207 
208 	/* already at >= 100% */
209 	if (state->priv->current >= state->priv->steps) {
210 		g_warning ("already at %u/%u steps on %p", state->priv->current, state->priv->steps, state);
211 		return;
212 	}
213 
214 	/* we have to deal with non-linear steps */
215 	if (state->priv->step_data != NULL) {
216 		/* we don't store zero */
217 		if (state->priv->current == 0) {
218 			parent_percentage = percentage * state->priv->step_data[state->priv->current] / 100;
219 		} else {
220 			/* bilinearly interpolate for XXXXXXXXXXXXXXXXXXXX */
221 			parent_percentage = (((100 - percentage) * state->priv->step_data[state->priv->current-1]) +
222 					     (percentage * state->priv->step_data[state->priv->current])) / 100;
223 		}
224 		goto out;
225 	}
226 
227 	/* get the offset */
228 	offset = cd_state_discrete_to_percent (state->priv->current, state->priv->steps);
229 
230 	/* get the range between the parent step and the next parent step */
231 	range = cd_state_discrete_to_percent (state->priv->current+1, state->priv->steps) - offset;
232 	if (range < 0.01) {
233 		g_warning ("range=%f (from %u to %u), should be impossible", range, state->priv->current+1, state->priv->steps);
234 		return;
235 	}
236 
237 	/* get the extra contributed by the child */
238 	extra = ((gfloat) percentage / 100.0f) * range;
239 
240 	/* emit from the parent */
241 	parent_percentage = (guint) (offset + extra);
242 out:
243 	cd_state_set_percentage (state, parent_percentage);
244 }
245 
246 /**
247  * cd_state_child_subpercentage_changed_cb:
248  **/
249 static void
cd_state_child_subpercentage_changed_cb(CdState * child,guint percentage,CdState * state)250 cd_state_child_subpercentage_changed_cb (CdState *child, guint percentage, CdState *state)
251 {
252 	/* discard this, unless the CdState has only one step */
253 	if (state->priv->steps != 1)
254 		return;
255 
256 	/* propagate up the stack as if the parent didn't exist */
257 	cd_state_set_subpercentage (state, percentage);
258 }
259 
260 /**
261  * cd_state_reset:
262  **/
263 gboolean
cd_state_reset(CdState * state)264 cd_state_reset (CdState *state)
265 {
266 	g_return_val_if_fail (CD_IS_STATE (state), FALSE);
267 
268 	/* reset values */
269 	state->priv->steps = 0;
270 	state->priv->current = 0;
271 	state->priv->last_percentage = 0;
272 
273 	/* only use the timer if profiling; it's expensive */
274 	if (state->priv->enable_profile)
275 		g_timer_start (state->priv->timer);
276 
277 	/* disconnect client */
278 	if (state->priv->percentage_child_id != 0) {
279 		g_signal_handler_disconnect (state->priv->child,
280 					     state->priv->percentage_child_id);
281 		state->priv->percentage_child_id = 0;
282 	}
283 	if (state->priv->subpercentage_child_id != 0) {
284 		g_signal_handler_disconnect (state->priv->child,
285 					     state->priv->subpercentage_child_id);
286 		state->priv->subpercentage_child_id = 0;
287 	}
288 
289 	/* unref child */
290 	if (state->priv->child != NULL) {
291 		g_object_unref (state->priv->child);
292 		state->priv->child = NULL;
293 	}
294 
295 	/* no more step data */
296 	g_free (state->priv->step_data);
297 	g_free (state->priv->step_profile);
298 	state->priv->step_data = NULL;
299 	state->priv->step_profile = NULL;
300 	return TRUE;
301 }
302 
303 /**
304  * cd_state_set_global_share:
305  **/
306 static void
cd_state_set_global_share(CdState * state,gdouble global_share)307 cd_state_set_global_share (CdState *state, gdouble global_share)
308 {
309 	state->priv->global_share = global_share;
310 }
311 
312 /**
313  * cd_state_get_child:
314  **/
315 CdState *
cd_state_get_child(CdState * state)316 cd_state_get_child (CdState *state)
317 {
318 	CdState *child = NULL;
319 
320 	g_return_val_if_fail (CD_IS_STATE (state), NULL);
321 
322 	/* already set child */
323 	if (state->priv->child != NULL) {
324 		g_signal_handler_disconnect (state->priv->child,
325 					     state->priv->percentage_child_id);
326 		g_signal_handler_disconnect (state->priv->child,
327 					     state->priv->subpercentage_child_id);
328 		g_object_unref (state->priv->child);
329 	}
330 
331 	/* connect up signals */
332 	child = cd_state_new ();
333 	child->priv->parent = state; /* do not ref! */
334 	state->priv->child = child;
335 	state->priv->percentage_child_id =
336 		g_signal_connect (child, "percentage-changed",
337 				  G_CALLBACK (cd_state_child_percentage_changed_cb),
338 				  state);
339 	state->priv->subpercentage_child_id =
340 		g_signal_connect (child, "subpercentage-changed",
341 				  G_CALLBACK (cd_state_child_subpercentage_changed_cb),
342 				  state);
343 	/* reset child */
344 	child->priv->current = 0;
345 	child->priv->last_percentage = 0;
346 
347 	/* set the global share on the new child */
348 	cd_state_set_global_share (child, state->priv->global_share);
349 
350 	/* set the profile state */
351 	cd_state_set_enable_profile (child,
352 				      state->priv->enable_profile);
353 
354 	return child;
355 }
356 
357 /**
358  * cd_state_set_number_steps_real:
359  **/
360 gboolean
cd_state_set_number_steps_real(CdState * state,guint steps,const gchar * strloc)361 cd_state_set_number_steps_real (CdState *state, guint steps, const gchar *strloc)
362 {
363 	g_return_val_if_fail (state != NULL, FALSE);
364 
365 	/* nothing to do for 0 steps */
366 	if (steps == 0)
367 		return TRUE;
368 
369 	/* did we call done on a state that did not have a size set? */
370 	if (state->priv->steps != 0) {
371 		g_warning ("steps already set to %u, can't set %u! [%s]",
372 			     state->priv->steps, steps, strloc);
373 		cd_state_print_parent_chain (state, 0);
374 		return FALSE;
375 	}
376 
377 	/* set id */
378 	g_free (state->priv->id);
379 	state->priv->id = g_strdup_printf ("%s", strloc);
380 
381 	/* only use the timer if profiling; it's expensive */
382 	if (state->priv->enable_profile)
383 		g_timer_start (state->priv->timer);
384 
385 	/* imply reset */
386 	cd_state_reset (state);
387 
388 	/* set steps */
389 	state->priv->steps = steps;
390 
391 	/* global share just got smaller */
392 	state->priv->global_share /= steps;
393 	return TRUE;
394 }
395 
396 /**
397  * cd_state_set_steps_real:
398  **/
399 gboolean
cd_state_set_steps_real(CdState * state,GError ** error,const gchar * strloc,gint value,...)400 cd_state_set_steps_real (CdState *state, GError **error, const gchar *strloc, gint value, ...)
401 {
402 	va_list args;
403 	guint i;
404 	gint value_temp;
405 	guint total;
406 
407 	g_return_val_if_fail (state != NULL, FALSE);
408 	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
409 
410 	/* we must set at least one thing */
411 	total = value;
412 
413 	/* process the valist */
414 	va_start (args, value);
415 	for (i = 0;; i++) {
416 		value_temp = va_arg (args, gint);
417 		if (value_temp == -1)
418 			break;
419 		total += (guint) value_temp;
420 	}
421 	va_end (args);
422 
423 	/* does not sum to 100% */
424 	if (total != 100) {
425 		g_set_error (error,
426 			     CD_STATE_ERROR,
427 			     CD_STATE_ERROR_INVALID,
428 			     "percentage not 100: %u",
429 			     total);
430 		return FALSE;
431 	}
432 
433 	/* set step number */
434 	if (!cd_state_set_number_steps_real (state, i+1, strloc)) {
435 		g_set_error (error,
436 			     CD_STATE_ERROR,
437 			     CD_STATE_ERROR_INVALID,
438 			     "failed to set number steps: %u",
439 			     i+1);
440 		return FALSE;
441 	}
442 
443 	/* save this data */
444 	total = value;
445 	state->priv->step_data = g_new0 (guint, i+2);
446 	state->priv->step_profile = g_new0 (gdouble, i+2);
447 	state->priv->step_data[0] = total;
448 	va_start (args, value);
449 	for (i = 0;; i++) {
450 		value_temp = va_arg (args, gint);
451 		if (value_temp == -1)
452 			break;
453 
454 		/* we pre-add the data to make access simpler */
455 		total += (guint) value_temp;
456 		state->priv->step_data[i+1] = total;
457 	}
458 	va_end (args);
459 	return TRUE;
460 }
461 
462 /**
463  * cd_state_show_profile:
464  **/
465 static void
cd_state_show_profile(CdState * state)466 cd_state_show_profile (CdState *state)
467 {
468 	gdouble division;
469 	gdouble total_time = 0.0f;
470 	guint i;
471 	guint uncumalitive = 0;
472 	g_autoptr(GString) result = NULL;
473 
474 	/* get the total time so we can work out the divisor */
475 	for (i = 0; i < state->priv->steps; i++)
476 		total_time += state->priv->step_profile[i];
477 	division = total_time / 100.0f;
478 
479 	/* what we set */
480 	result = g_string_new ("steps were set as [ ");
481 	for (i = 0; i < state->priv->steps; i++) {
482 		g_string_append_printf (result, "%u, ",
483 					state->priv->step_data[i] - uncumalitive);
484 		uncumalitive = state->priv->step_data[i];
485 	}
486 
487 	/* what we _should_ have set */
488 	g_string_append_printf (result, "-1 ] but should have been: [ ");
489 	for (i = 0; i < state->priv->steps; i++) {
490 		g_string_append_printf (result, "%.0f, ",
491 					state->priv->step_profile[i] / division);
492 	}
493 	g_printerr ("\n\n%s-1 ] at %s\n\n", result->str, state->priv->id);
494 }
495 
496 /**
497  * cd_state_done_real:
498  **/
499 gboolean
cd_state_done_real(CdState * state,GError ** error,const gchar * strloc)500 cd_state_done_real (CdState *state, GError **error, const gchar *strloc)
501 {
502 	gdouble elapsed;
503 	gfloat percentage;
504 
505 	g_return_val_if_fail (state != NULL, FALSE);
506 	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
507 
508 	/* did we call done on a state that did not have a size set? */
509 	if (state->priv->steps == 0) {
510 		g_set_error (error, CD_STATE_ERROR, CD_STATE_ERROR_INVALID,
511 			     "done on a state %p that did not have a size set! [%s]",
512 			     state, strloc);
513 		cd_state_print_parent_chain (state, 0);
514 		return FALSE;
515 	}
516 
517 	/* save the step interval for profiling */
518 	if (state->priv->enable_profile) {
519 		/* save the duration in the array */
520 		elapsed = g_timer_elapsed (state->priv->timer, NULL);
521 		if (state->priv->step_profile != NULL)
522 			state->priv->step_profile[state->priv->current] = elapsed;
523 		g_timer_start (state->priv->timer);
524 	}
525 
526 	/* is already at 100%? */
527 	if (state->priv->current >= state->priv->steps) {
528 		g_set_error (error, CD_STATE_ERROR, CD_STATE_ERROR_INVALID,
529 			     "already at 100%% state [%s]", strloc);
530 		cd_state_print_parent_chain (state, 0);
531 		return FALSE;
532 	}
533 
534 	/* is child not at 100%? */
535 	if (state->priv->child != NULL) {
536 		CdStatePrivate *child_priv = state->priv->child->priv;
537 		if (child_priv->current != child_priv->steps) {
538 			g_print ("child is at %u/%u steps and parent done [%s]\n",
539 				 child_priv->current, child_priv->steps, strloc);
540 			cd_state_print_parent_chain (state->priv->child, 0);
541 			/* do not abort, as we want to clean this up */
542 			return TRUE;
543 		}
544 	}
545 
546 	/* another */
547 	state->priv->current++;
548 
549 	/* find new percentage */
550 	if (state->priv->step_data == NULL) {
551 		percentage = cd_state_discrete_to_percent (state->priv->current,
552 							    state->priv->steps);
553 	} else {
554 		/* this is cumalative */
555 		percentage = state->priv->step_data[state->priv->current - 1];
556 	}
557 	cd_state_set_percentage (state, (guint) percentage);
558 
559 	/* show any profiling stats */
560 	if (state->priv->enable_profile &&
561 	    state->priv->current == state->priv->steps &&
562 	    state->priv->step_profile != NULL) {
563 		cd_state_show_profile (state);
564 	}
565 
566 	/* reset child if it exists */
567 	if (state->priv->child != NULL)
568 		cd_state_reset (state->priv->child);
569 	return TRUE;
570 }
571 
572 /**
573  * cd_state_finished_real:
574  **/
575 gboolean
cd_state_finished_real(CdState * state,GError ** error,const gchar * strloc)576 cd_state_finished_real (CdState *state, GError **error, const gchar *strloc)
577 {
578 	g_return_val_if_fail (state != NULL, FALSE);
579 	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
580 
581 	/* is already at 100%? */
582 	if (state->priv->current == state->priv->steps)
583 		return TRUE;
584 
585 	/* all done */
586 	state->priv->current = state->priv->steps;
587 
588 	/* set new percentage */
589 	cd_state_set_percentage (state, 100);
590 	return TRUE;
591 }
592 
593 /**
594  * cd_state_finalize:
595  **/
596 static void
cd_state_finalize(GObject * object)597 cd_state_finalize (GObject *object)
598 {
599 	CdState *state;
600 
601 	g_return_if_fail (object != NULL);
602 	g_return_if_fail (CD_IS_STATE (object));
603 	state = CD_STATE (object);
604 
605 	cd_state_reset (state);
606 	g_free (state->priv->id);
607 	g_free (state->priv->step_data);
608 	g_free (state->priv->step_profile);
609 	g_timer_destroy (state->priv->timer);
610 
611 	G_OBJECT_CLASS (cd_state_parent_class)->finalize (object);
612 }
613 
614 /**
615  * cd_state_class_init:
616  **/
617 static void
cd_state_class_init(CdStateClass * klass)618 cd_state_class_init (CdStateClass *klass)
619 {
620 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
621 	object_class->finalize = cd_state_finalize;
622 
623 	signals [SIGNAL_PERCENTAGE_CHANGED] =
624 		g_signal_new ("percentage-changed",
625 			      G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
626 			      G_STRUCT_OFFSET (CdStateClass, percentage_changed),
627 			      NULL, NULL, g_cclosure_marshal_VOID__UINT,
628 			      G_TYPE_NONE, 1, G_TYPE_UINT);
629 
630 	signals [SIGNAL_SUBPERCENTAGE_CHANGED] =
631 		g_signal_new ("subpercentage-changed",
632 			      G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
633 			      G_STRUCT_OFFSET (CdStateClass, subpercentage_changed),
634 			      NULL, NULL, g_cclosure_marshal_VOID__UINT,
635 			      G_TYPE_NONE, 1, G_TYPE_UINT);
636 
637 	g_type_class_add_private (klass, sizeof (CdStatePrivate));
638 }
639 
640 /**
641  * cd_state_init:
642  **/
643 static void
cd_state_init(CdState * state)644 cd_state_init (CdState *state)
645 {
646 	state->priv = CD_STATE_GET_PRIVATE (state);
647 	state->priv->global_share = 1.0f;
648 	state->priv->timer = g_timer_new ();
649 }
650 
651 /**
652  * cd_state_new:
653  **/
654 CdState *
cd_state_new(void)655 cd_state_new (void)
656 {
657 	CdState *state;
658 	state = g_object_new (CD_TYPE_STATE, NULL);
659 	return CD_STATE (state);
660 }
661